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 мастера, если оно когда-нибудь понадобится.