Команда может честно показать валидный OpenAPI-документ и всё равно выпустить несовместимое изменение. Это происходит, когда provider сохраняет форму: ключи те же, типы те же, null разрешён, HTTP-статус успешный. Но consumer использовал поле как обещание действия. В учебном ответе state: "active" остаётся допустимым, а renewalAt становится null; структура не возражает, экран продления теряет свою предпосылку.
Цена ошибки в том, что разные команды начинают спорить об одном слове разными фактами. Provider говорит «схема не менялась», consumer говорит «сценарий сломан», release engineer видит зелёную проверку JSON и не понимает, должен ли остановить выпуск. Без разделения уровней любая из этих фраз выглядит убедительно и ни одна не даёт решения. Нужна модель, которая показывает, какое наблюдение отвечает на какой вопрос и какой PASS разрешает следующий шаг.
Слой 1: schema описывает форму и её допустимую вариативность
Schema полезна именно потому, что делает форму проверяемой. В OAS 3.1.0 Schema Object может описать типы, перечисления, composition и пример. Для renewalAt учебный договор допускает ISO-строку или null, а для state — active или paused. Такой контракт помогает заметить исчезнувший обязательный ключ, число вместо строки и неизвестное значение enum до того, как это увидит пользователь.
Однако форма не создаёт предметный смысл автоматически. Тот же OAS говорит, что для свойств, чья семантика задаётся приложением, спецификация оставляет её потребляющему приложению. Это не недостаток стандарта: API может обслуживать разные клиентов, и одна общая schema не знает, какое действие должен показать конкретный экран. Ошибка начинается, когда команда прочитывает технический PASS как обещание бизнес-поведения, которое не было записано.
| Проверка | Вход active + null | Что можно сказать после PASS | Чего говорить нельзя |
|---|---|---|---|
| Schema validator | форма допустима | ключи и типы соответствуют описанию | экран продления может продолжить сценарий |
| Consumer semantic expectation | условие не выполнено | consumer явно заметил недостающую дату | provider был запущен или найден дефект |
| Provider verification | требует отдельного исполнения | конкретный provider удовлетворил конкретные interactions | все API-клиенты и состояния совместимы |
| Release gate | требует версий и результатов | можно принять решение в заранее названной границе | последствий после выпуска не будет |
Слой 2: semantic expectation принадлежит сценарию consumer
Semantic expectation должна быть короткой и наблюдаемой. В этом пакете она звучит так: «для сценария экрана продления ответ с state: active содержит календарно валидную renewalAt позже фиксированного synthetic reference time». Это не определение active для всего домена и не указание provider менять модель; reference time нужен только детерминированному fixture и не является временем реальной системы. Это контракт одной потребности consumer. Если для другого клиента active + null допустим, его scenario должен быть записан отдельно, а не размыт внутри общего enum.
У такого правила есть две части. Первая — context: какой request, provider state и путь пользователя привели к ответу. Вторая — decision: что consumer делает при значении. Если оставить только пример JSON без решения, он легко превратится в случайный fixture. Если оставить только фразу «нужна дата», reviewer не поймёт, при каком state provider обязан её вернуть. Связка context + decision даёт проверяемый контракт, но ещё не подтверждает поведение настоящего provider.
Слой 3: provider verification — выполненный факт, а не поле в JSON
В Pact consumer-driven contract формируется при запуске consumer tests как набор конкретных request/response interactions. Дальше provider verification воспроизводит их против provider в подготовленном состоянии. Успешная проверка должна иметь адресуемый результат: какой contract, какая версия provider, какой provider state и где записан PASS/FAIL. Без этих четырёх частей слово «verified» нельзя отличить от ручного предположения.
Важно не подменять этот процесс проверкой schema. Provider может отдавать структурно валидный ответ и всё же не удовлетворять примеру, который consumer опубликовал для нужного state. И наоборот, строгий consumer может ожидать лишнее поле, хотя schema и другие клиенты его не требуют. Поэтому contract review должен спросить: это общая форма интерфейса, ожидание конкретного consumer или уже выполненная verification? Ответ выбирает следующий механизм, а не победителя в споре.
Исполнимый fixture отделяет три флага
В примере ниже сначала создаётся только marked synthetic contract. Затем в память передаётся ответ, который структурно допустим, но нарушает expectation экрана. Функция возвращает три раздельных поля: schemaMatch, semanticExpectationMatch и actualProviderVerification. Последнее остаётся not-executed при любом входе; это защита от ложного вывода «fixture проверил provider».
import {
createSyntheticContract,
inspectSyntheticProviderResponse,
} from './web/scripts/upgrade-2023-07.mjs';
const contract = createSyntheticContract({
marker: 'synthetic-contract-input-v1',
consumer: 'synthetic-portal-web',
provider: 'synthetic-billing-api',
operation: 'GET /v1/subscriptions/{id}',
scenario: 'renewal-screen-needs-an-actionable-date',
});
const changedMeaning = inspectSyntheticProviderResponse(contract, {
marker: 'synthetic-provider-response-v1',
value: { id: 'sub-42', state: 'active', renewalAt: null },
});
console.log(changedMeaning.schemaMatch); // true
console.log(changedMeaning.semanticExpectationMatch); // false
console.log(changedMeaning.actualProviderVerification); // not-executed
Если выполнить команду fixture, она дополнительно проверит отрицательные ветки: object без synthetic marker не принимается, число в state и несуществующая календарная дата ломают schema, а active + null и дата до synthetic reference time проходят форму, но не проходят semantic expectation. Результат syntheticCriteriaWouldPass означает лишь, что правила in-memory выполнены. Он не публикует verification result, не знает версию брокера и не пытается вычислить production compatibility.
Маршрут: симптом → причина → проверка → действие
- Симптом. Найдите пример, где ответ выглядит корректно в логе, но consumer не может завершить конкретное действие. Не начинайте со слова «ломается API».
- Причина. Разделите форму и смысл. Выпишите, какие значения schema допускает намеренно, и какое из них consumer интерпретирует уже, чем provider.
- Проверка schema. Сверьте версию OAS, required-поля, типы, enum и nullable. Если ответ здесь FAIL, не обсуждайте семантику, пока не названо структурное нарушение.
- Проверка смысла. Запишите provider state и decision consumer. Для правила active + date укажите, что должно произойти, если дата отсутствует: другой экран, явный отказ или временное сообщение.
- Проверка provider. Подготовьте реальную execution-среду и привяжите её к версии contract и provider. Не переназывайте synthetic fixture в verification.
- Действие. Выберите границу изменения: добавить новое явное поле, ввести новый state, сохранить старое значение на период миграции или остановить выпуск до согласования клиентов.
Почему один contract не отменяет версионную работу
Даже хороший пример покрывает только известную потребность. Consumer-driven подход намеренно тестирует используемые interactions, а не весь бесконечный набор возможных состояний provider. Это делает feedback быстрым, но требует честного inventory: какие consumers известны, какие версии ещё поддерживаются, кто владеет provider state и как публикуется результат. Иначе команда создаёт contract, который защищает новый клиент, но случайно объявляет безопасным выпуск для старого.
Версия OAS тоже должна быть названа рядом с документом. OAS 3.1.0 определяет свою семантику и совместимость tooling в линии 3.1.*, но API implementation version — отдельное понятие. Нельзя заменить версию provider номером спецификации или наоборот. В release decision полезно хранить оба указателя: какой API contract потреблял consumer и какую сборку provider фактически проверяли. Это избавляет расследование от догадки по дате merge.
Ограничения, rollback и следующий шаг
Этот fixture не моделирует реальные HTTP-заголовки, auth, retries, provider states, broker, matcher-алгоритм Pact, consumer client или зависимости provider. Он не валидирует OpenAPI-документ и не сравнивает deployed версии. Synthetic значения выбраны только для объяснения границы active + null. Поэтому нельзя читать его PASS как утверждение о реальном контракте или о том, что достаточно сделать поле обязательным.
Rollback учебной модели восстанавливает snapshot contract-2023-07-baseline и специально сообщает deployment: not-performed. В настоящем выпуске надо отдельно проверить, не начал ли consumer писать новое значение, не использует ли provider новое значение в данных и можно ли временно отдать обе интерпретации. Следующий шаг — добавить к одному реальному PR таблицу из этой статьи и потребовать два разных evidence: schema diff и результат provider verification для точных версий.
Историческая граница июля 2023
Описанная модель использует только возможности и терминологию, доступные к июлю 2023: OAS 3.1.0 был опубликован в феврале 2021, а страницы Pact о contract by example и verification были обновлены в августе 2022. Поздние инструменты, результаты запусков и состояние конкретных библиотек сюда не переносятся задним числом.
Проверяемые источники
- OpenAPI Specification v3.1.0, 15 февраля 2021 — версионная первичная спецификация, доступная до июля 2023. OAS описывает интерфейс HTTP API и типы; для части свойств она прямо оставляет прикладную семантику потребляющему приложению.
- Pact Docs: Introduction to contract testing, обновлено 30 августа 2022 — официальная документация Pact, исторически доступная до июля 2023. Она различает статическую schema/specification и contract by example, который формируется выполнением consumer tests.
- Pact Docs: Verifying Pacts, обновлено 11 августа 2022 — официальная документация Pact, исторически доступная до июля 2023. В ней provider verification привязан к локальному provider или CI, состояниям provider и опубликованному результату; этот sidecar такого запуска не выполняет.