Что и чем проверяем
Transform → Load одинаков для всех источников — его берёт
детерминированный юнит на синтетических RawItem.
Source-specific только Extract и normalize: их тестируем
против записанных ответов и образов, а живьём — лишь вручную.
Три уровня по воспроизводимости
Воспроизводимость растёт от живого источника к образу и к
HTTP-фикстуре: живые free-tier — ручная отладка, вне CI; Docker-образы —
интеграция отдельным, более редким шагом CI; HTTP-фикстуры
(VCR.py / WireMock) — юнит, контракт и сквозной replay по источникам
на каждом PR. В make test входит только фикстурный
уровень; Docker-интеграция идёт отдельным шагом.
API-слой проверяет край, не повторяет домен
HTTP-тесты держат границу: форма запроса и валидация схемы, коды
ответов, маска секретов, авторизация на каждом роуте. Глубину
поведения — замки прогона, каскады удаления, три шага Test
Connection — держат доменные тесты выше; эндпоинт обязан лишь
корректно отразить их наружу. Сквозной механикой RBAC / JWT / CORS
владеет Auth & Security, здесь — только что роуты Harvester за
этим guard.
Контракт-кейсы — и kit для автора коннектора
Тот же набор, которым платформа проверяет встроенные коннекторы
(test_connector_contract +
test_manifest_registry), автор внешнего коннектора
прогоняет против своего класса — проверка соответствия контракту до
поставки. Контракт до 1.0 не заморожен: kit меняется
вместе с ним.
Стек и инфраструктура
Имеется donepytest, pytest-asyncio, pytest-cov, httpx
filter — встроенные правилапустые сущности, системные события, боты отсеиваются; базовые правила не настраиваются
filter — манифестпереопределения и доп-включения берутся из content_filters манифеста
classify → статуссущности проставляется draft / final / archived
resolve — дубли в прогонесхлопывание дублей внутри одного прогона + выбор победителя версии; межпрогонная идемпотентность — НЕ здесь
enrich — связи и refsребро строится сразу, когда целевой узел уже загружен; иначе (чужой источник или forward) — заявкой refs, ребро не материализуется
clean — копия под эмбеддингчистится после enrich и только копия; оригинал в Entity нетронут
embed — модель и кешкаждый фрагмент → вектор моделью из реестра AI; кеш по контент-хешу, неизменные не переэмбеддятся
embed — назначение обязательнобез назначенной embedding-функции приём не стартует (дефолта нет); назначена встроенная модель платформы → приём идёт, фрагменты эмбеддятся ею
upsert — один ключпо source_id + source_type + source_entity_id ровно одна строка; повторный прогон обновляет, не плодит дубли
синхронизация фрагментовнабор векторов сверяется целиком: устаревшие удалены, новые добавлены, осиротевших нет
неизменный фрагментвектор по контент-хешу — не переэмбеддится при повторе
три проекцииодна Entity → relational + vector + graph (раскладка полей — за Knowledge Store)
ACL фрагментафрагмент наследует ACL сущности, своего не имеет
удаление — отдельная осьпропавшая в источнике запись получает маркер удаления; статус классификации (draft / final / archived) сохраняется как был — скрыта из выдачи, физически хранится
восстановлениемаркер снят (запись вернулась / удаление было ошибкой) → прежний статус возвращается сам, без переклассификации
ContractКоннекторы — единый интерфейс на HTTP-фикстурахVCR.py + WireMock · каждый PR
параметризация по типуодин прогон на каждый встроенный тип — Jira / GitLab / Slack — из своей закоммиченной кассеты
весь конвейер офлайнfetch из кассеты → Transform → Load без сети и без образа; сквозное покрытие там, где Docker даёт лишь Git-семейство (у Slack/Jira образа для регулярного CI нет)
эталон сущностейчисло, типы и ключи сущностей сверяются с golden-снимком; расхождение — провал, не warning
детерминизм прогонафиксированный сид чанков и эмбеддинга → повтор бит-в-бит, дифф читаем в ревью
перезапись кассетыдрейф API чинится перезаписью из manual/; кассета — закоммиченный файл, в CI сети не трогает и не «отваливается»
Integration · P0Сквозной конвейер против образаDocker · отдельный шаг CI
test_pipeline_e2e.py
Extract → Transform → Load против засеянного источника
freshness-бюджетразрыв > 6 ч → чекпоинт отброшен, прогон с нуля; cancelled не возобновляется
retry → DLQbackoff на обрыв / таймаут / 429 / 5xx; после исчерпания item уходит в dead_letters, не теряется
перманентная → сразу в DLQ400 / 401 / 404 / 422 и ошибки нормализации — повтор бессмыслен, элемент уходит в dead_letters без ретраев
транзиентная → ретрайвсплеск нагрузки / кратковременная недоступность — ретрай с backoff; класс ошибки решает, не буква кода
коннектор переопределяет класс403 у GitHub / GitLab — чаще rate-limit: по Retry-After / X-RateLimit-Remaining коннектор трактует его транзиентным, а не перманентным
два уровня ретраевкороткая задержка (≤ 60 с) — in-memory удержание страницы в задаче; долгая пауза (Retry-After в минуты-часы) → SAQ defer / re-enqueue, слот воркера освобождён, не sleep
частичный отказпрогон succeeded с error_count > 0 — отличается от failed
уведомление о провалеfailed-прогон поднимает уведомление; выбор канала — за модулем уведомлений, Harvester лишь сигналит
один активный прогонpartial unique index UNIQUE (source_id) WHERE state IN ('queued','running')
дедуп DLQтот же item обновляет строку (attempts, reason), не плодит дубль; обработанный — удаляется
детекция удаления mode-специфичнаIncremental не ставит маркер исчезновения (удаление в дельту не приходит); Reconciliation полным сканом помечает пропавшее
partial_resync (12 ч)молчание > 12 ч → дешёвый polling → дельта есть → пересбор окна, авто, без человека
dlq_retry — ручнойточечный повтор по identity из очереди после починки; обработанные уходят из DLQ
per-sourceкаждый источник держит свой since / график / режим; «синхронизировать всё» = запуск всех разом
подлинностьHMAC над телом (GitHub / Slack), статичный токен в заголовке (GitLab), fallback — секретный endpoint
верификатор по провайдерупараметризация подключаемого верификатора по 4 провайдерам, включая Atlassian (JWT Connect-приложения); ядро прогоняет единый порядок свежесть → подпись → дедуп
не подтверждена → отказподделанная подпись / токен → вызов отклонён
свежесть меткигде есть timestamp (Slack, GitLab) — вызов старше окна 5 мин отброшен до подписи; у источника без метки (GitHub, Atlassian) шаг пропускается, анти-replay целиком несёт дедуп (TTL окна больше — 24 ч)
стартовый темп нащупывает границустарт с темпа из манифеста; при спокойных ответах AIMD-разгон +10% за окно — темп нащупывает реальную границу вверх, не только откатывается по 429
сигнал провайдера сильнее AIMDRetry-After — жёсткая пауза на весь scope, перекрывает адаптивный темп; уважают все воркеры
упреждение по RemainingX-RateLimit-Remaining мал → тормозим заранее: safe_rps = Remaining / (Reset − now), берём min с темпом AIMD
учёт по стоимостилимитер списывает единицы стоимости (cost-units), не число запросов; сколько единиц стоит запрос — объявляет коннектор (по умолчанию 1)
ключ по rate_limit_scopeключ лимитера строится по rate_limit_scope манифеста (tenant / account_token / workspace_method / site); два источника на одном токене делят бюджет
темп переживает прогонывыученная ёмкость API не сбрасывается между прогонами — живёт в Redis с TTL в часы, переживает рестарт воркера
разрешение расписанияper-source sync_interval / reconcile_interval = NULL → наследует глобальный дефолт; задан → идёт по своему
reconciliation по графикусуточный / недельный прогон сверки запускается планировщиком, не вручную
health-check по расписаниюоблегчённая проба (шаги 1–2 Test Connection) между синхронизациями; провал → health error + уведомление
watchdog по таймерумолчание > 12 ч → watchdog сам поднимает partial_resync, без человека
«синхронизировать всё» = веерзапуск всех per-source разом; независимые источники бегут в running параллельно — partial index по source_id, не глобальный
API · P1Эндпоинты источников — HTTP-контрактhttpx · ASGI-приложение · БД · фейковый коннектор · → HTTP API · → Conformance
созданиевалидная форма → 201 + тело с id; следом авто Full Sync (trigger=connect)
форма из манифестаполя и виды credential валидируются по манифесту коннектора; лишнее / чужое поле → 422 VALIDATION_ERROR
CHECK наружу как 4xxнедопустимый auth_method / scope_mode, пустое name → 422, не 500
тип вне реестранеизвестный connector_type → 422 (сверка с реестром, не enum БД)
чтениесписок и деталь → state + выведенный health + последний прогон; счётчик сущностей из Knowledge Store
маска секретаGET / list отдают credential и webhook под маской; открытое значение — только в ответе на создание
правка конфигаPATCH частично; смена credential → перешифровка, наружу новая маска, не значение
нет ресурсанесуществующий id → 404 NOT_FOUND
health — лёгкая пробаGET /health → state + вычисленный health (idle / syncing / error) + last_probe_status, без полной детали источника — дешёвый эндпоинт для частого поллинга
черновик и существующийпроба на конфиге из мастера и по id источника → 200 с поэтапным результатом
3 шага раздельноURL недоступен / креды невалидны / прав мало → разные машинные коды шага, не общий 500
шаг 2 проваленшаг 3 в ответе помечен «не проверялся»
каталог после шага 2GET каталога объектов доступен только после успешных кредов; до — 409 / пусто
плановая проба делит контрактфоновый health-check пишет last_probe_status (ok / unreachable / auth_failed) и роняет health error — та же логика, что ручная
анонимбез токена → 401 UNAUTHORIZED (параметризовано по всем роутам)
истёкший токенaccess JWT старше 15 мин → 401
нет «голых» роутовкаждый write-эндпоинт реально за require(permission) — параметризованный аудит покрытия guard
API-ключ — только чтениеread-only ключ на write-операцию → отказ; шире прав владельца не бывает
webhook вне сессиипубличный приём — подписанный канал, не JWT; его контракт держит test_webhook_security
ManualЖивые free-tier источники — вне CIручная отладка · источник истины для фикстур
Бесплатные тарифы реальных SaaS с насеянными sample-данными.
Назначение — ручная отладка нового коннектора вживую: видно
фактическое поведение API, формы ответов, особенности пагинации и
прав, которые не воспроизвести моком. Нестабильны, требуют токенов и
сети — в автоматический прогон не входят; служат источником истины
при записи HTTP-фикстур (токены вычищаются при записи).
AtlassianAtlassian Cloud free — Jira + Confluence через
atlassian-python-api; реальная модель прав и
пагинация
Slackбесплатный workspace через slack-sdk — каналы, треды,
rate-limit вживую
GitLabgitlab.com + насеянный проект через
python-gitlab — issues, MR, wiki
StructureФайловая структура тестов
Разделение задаётся не папками, а назначением. unit/,
contract/ и replay/ входят в make test
и идут на каждом PR (детерминированно). integration/ и
api/ — отдельным, более редким шагом: первый на
Docker-образах, второй на поднятом приложении и БД. Живые источники
остаются ручным инструментом отладки и в автоматический прогон не
попадают. Приоритет (P0–P1) ортогонален каталогам и задаётся маркерами
(pytest -m p0).