Перейти к основному содержимому

ADR 0120 — Браузерный E2E против продакшен-compose, в пакете вне workspace

  • Статус: принят
  • Дата: 2026-08-29
  • Ссылки (без правки): ADR 0032 (сессия в cookie, доказывать которую существует этот слой), ADR 0117 (прецедент поддерева только для разработки с собственным lockfile), ADR 0027 (продакшен-smoke, который этот слой переиспользует вместо дублирования).

Контекст​

Пирамида тестов была крепкой внизу и имела дыру наверху. У api 142 spec, у engine 126, web тестируется покомпонентно под jsdom, а docker/smoke.sh доказывает по HTTP, что четыре продакшен-образа поднимаются и общаются между собой.

Ни один из них не задействует браузер. А механизмы, которые несут сессию, — это механизмы браузера:

  • access token живёт в памяти, а refresh — в cookie httpOnly, в паре с читаемым brabo_csrf, отправляемым как X-CSRF-Token;
  • web отдаётся на :8088, а api отвечает на :3000, поэтому каждый аутентифицированный вызов — кросс-доменный: preflight, SameSite, credentials: 'include';
  • канал сессии требует одноразового тикета (RN-108) и настоящего WebSocket-рукопожатия с engine на третьем origin.

В jsdom ничего этого нет. У него нет настоящего origin, он не делает preflight, а httpOnly он навязать не способен — тест, утверждающий, что refresh нечитаем, прошёл бы там, ничего не доказав. В apps/api/src/main.ts уже есть комментарий, говорящий это прямым текстом: "teste não faz preflight."

Это не гипотетический пробел. Именно там на деле проявились последние дефекты cookie, CORS и сокета, и каждый был найден вручную.

Решение​

Слой браузерного E2E в e2e/, работающий против продакшен-compose, который CI уже поднимает.

В решении три части, у каждой своя причина.

Работает против docker-compose.prod.yml, а не против vite dev​

Разделение origin (:8088 → :3000 → :4000) существует только в продакшен-compose. Запуск против dev-сервера задействовал бы топологию, которую никто не развёртывает, и молча перестал бы доказывать единственное, ради чего существует этот слой. Поэтому job переиспользует docker/smoke.sh с SMOKE_KEEP_UP=1: те же образы, тот же compose, одна сборка.

Он также работает в том же job CI, что и образы. Четыре образа — это большая часть времени этого job; отдельный job пересобирал бы их, чтобы задействовать тот же стек.

e2e/ не является членом pnpm workspace​

Та же форма, что у website/ (ADR 0117): собственные pnpm-workspace.yaml и pnpm-lock.yaml, установка через pnpm install изнутри папки, запуск из корня через pnpm --dir e2e.

Причина та же: дерево Playwright никогда не попадает ни в один образ продукта, и оставив его в корневом lockfile, pnpm audit продукта сообщал бы о тестовом инструменте как о поставляемой поверхности. Отличие от website — этот пакет тестирует продукт — не меняет аргумента, потому что решает то, куда идёт зависимость, а не то, о чём она говорит.

Доказывает механизмы, а не экраны​

Spec утверждают о наблюдаемом механизме: флаг httpOnly, как его сообщает браузер, document.cookie, как его видит страница, переживание reload, URL WebSocket, несущий ticket=. Селекторы структурные (input[type="email"]), никогда не текст — язык интерфейса решает сервер, и тест, привязанный к "Sign in", сломался бы при смене языка — сбой, который ничего не говорит о продукте.

Засев (workspace → проект → сессия) идёт по HTTP, повторяя smoke.sh. Браузер дорог, а мастер проекта — не то, ради доказательства чего существует этот слой; медленная подготовка — это подготовка, которую в итоге отключают.

Последствия​

  • Пробел закрывается для путей, которых достигает только браузер. Оба spec были доказаны мутацией: неверный пароль делает spec аутентификации красными, а направление проверки сокета на несуществующий путь падает с "nenhum WebSocket foi aberto contra o engine."
  • Job images растёт: настройка pnpm/Node, загрузка chromium и само выполнение. Взамен он остаётся одним job и одной сборкой образов.
  • Только Chromium. Один браузер доказывает механизм; два других стоили бы загрузки и времени, чтобы доказать то же самое снова. Различия между браузерами — реальный риск, который это решение объявляет непокрытым.
  • Бинарник браузера скачивается во время CI с CDN Playwright. Его версия закреплена точной версией @playwright/test в lockfile, и так эта загрузка вписывается в дисциплину pin репозитория — см. cadeia-de-suprimentos-do-ci.md.
  • Одна повторная попытка в CI, ни одной локально. E2E против живого стека имеет нестабильность с внешней причиной (контейнер ещё поднимается, медленный сокет). Ноль повторных попыток превратил бы это в шум, приучающий людей игнорировать красное.
  • Два spec — это минимум, а не набор. Inline-одобрение и streaming — названные в исходной находке — пока не покрыты.
  • У набора есть бюджет входов, и он появился из реального прогона. api защищает /auth/login прогрессивной блокировкой по IP, которая отвечает тем же единообразным 401, что и неверный пароль, — различение их сказало бы атакующему, когда он угадал e-mail. Повторный прогон набора в пределах 15-минутного окна её срабатывает, и тогда сбой обвиняет вход, где дефекта нет. Сделаны два вывода: браузер входит один раз за прогон (проект setup сохраняет состояние; лишь autenticacao.spec.ts остаётся вне, потому что доказательство входа требует чистого origin), а suporte/api.ts распознаёт этот 401 и говорит, чем он вероятно является. Альтернатива — просить у compose более мягкий порог, чтобы набор мог входить сколько угодно, — отвергнута: это ослабило бы проверяемое ради удобства теста.

Рассмотренные альтернативы​

e2e/ внутри pnpm workspace. Одна установка, без второго lockfile. Отвергнуто по причине, за которую только что заплатил ADR 0117: аудит продукта нёс бы дерево Playwright.

Запуск против vite dev. Быстрее, без сборки образов. Отвергнуто, потому что тестировалась бы неразвёртываемая топология, а dev-сервер с одним origin делает пустой каждую проверку autenticacao.spec.ts.

Выделенный job E2E. Более чистое разделение, и сбой там не сообщался бы под "images". Отвергнуто из-за стоимости: пересобирались бы четыре образа, чтобы прийти к тому же compose.

Проводить создание проекта через мастер вместо засева по HTTP. Реалистичнее, и покрыло бы мастер. Пока отвергнуто — это сделало бы выполнение в разы длиннее и связало бы этот слой с git-провайдером ради покрытия, которое принадлежит собственному spec мастера, если оно когда-нибудь понадобится.