ADR 0120 — 针对生产 compose 的浏览器 E2E,放在 workspace 之外的包中
- 状态: 已接受
- 日期: 2026-08-29
- 引用(不编辑): ADR 0032(这一层存在要证明的 cookie 会话)、ADR 0117(只用于开发、带自己 lockfile 的子树的先例)、ADR 0027(这一层复用而不是复制的生产冒烟测试)。
背景
测试金字塔底部坚实,顶部有个洞。api 有 142 个 spec,engine 有 126 个,web 在 jsdom 下逐个组件测试,docker/smoke.sh 通过 HTTP 证明四个生产镜像能启动并彼此对话。
它们都没有演练浏览器。而承载会话的机制是浏览器的机制:
- access token 存在内存中,refresh 在
httpOnlycookie 中,与可读的brabo_csrf配对,后者作为X-CSRF-Token发送; - web 在
:8088提供,api 在:3000响应,所以每个已认证调用都是跨源的——预检、SameSite、credentials: 'include'; - 会话通道需要一次性 ticket(RN-108),以及在第三个源上对 engine 的真实 WebSocket 握手。
jsdom 这些都没有。它没有真实的源,不做预检,httpOnly 也不是它能强制执行的东西——一个断言 refresh 不可读的测试在那里会通过而什么都没证明。apps/api/src/main.ts 已经带有把这一点说得清清楚楚的注释:“teste não faz
preflight.”
这不是假设的缺口。最近的 cookie、CORS 和 socket 缺陷确实出现在那里,每一个都是手工发现的。
决策
一个浏览器 E2E 层,位于 e2e/,针对 CI 已经启动的生产 compose 运行。
这个决定有三部分,各有其理由。
针对 docker-compose.prod.yml 运行,而不是 vite dev
源的分离(:8088 → :3000 → :4000)只存在于生产 compose 中。针对开发服务器运行会演练一个没人部署的拓扑,并会静默地不再证明这一层存在要证明的唯一东西。所以作业以 SMOKE_KEEP_UP=1 复用 docker/smoke.sh:相同的镜像,相同的 compose,一次构建。
它还在镜像的同一个 CI 作业中运行。四个镜像占了那个作业的大部分时间;单独的作业会重新构建它们来演练同一个栈。
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、重新加载后的存活、带有 ticket= 的 WebSocket URL。选择器是结构性的(input[type="email"]),绝不是文本——界面的语言由服务器决定,绑定在“Sign in”上的测试会在语言切换时失败,而这种失败对产品什么也说明不了。
播种(workspace → 项目 → 会话)通过 HTTP 进行,镜像 smoke.sh。浏览器很贵,而项目向导不是这一层存在要证明的东西;慢的准备就是最终会被关掉的准备。
后果
- 对于只有浏览器能到达的路径,缺口关闭了。两个 spec 都经过变异证明:错误的密码让认证 spec 变红,而把 socket 的断言指向一个不存在的路径会以“nenhum WebSocket foi aberto contra o engine.”失败。
images作业增长了:pnpm/Node 的设置、chromium 的下载以及执行本身。作为交换,它仍然是一个作业和一次镜像构建。- 只有 Chromium。一个浏览器就证明了机制;另外两个会花下载和时间来再次证明同一件事。浏览器之间的差异是真实的风险,本决定声明不覆盖它。
- 浏览器的二进制在 CI 时从 Playwright 的 CDN 下载。它的版本由 lockfile 中
@playwright/test的精确版本固定,这就是这次下载如何融入仓库的固定纪律——见 cadeia-de-suprimentos-do-ci.md。 - CI 中重试一次,本地不重试。 针对活栈的 E2E 有外因造成的不稳定(容器仍在启动、socket 很慢)。零重试会把这变成教人忽视红色的噪声。
- 两个 spec 是下限,不是套件。内联审批和流式传输——在原始发现中点名的——尚未被覆盖。
- 套件有登录预算,而它来自真正运行它。 api 以按 IP 的渐进封锁保护
/auth/login,响应的是与错误密码相同的统一 401——区分它们会告诉攻击者他何时猜对了电子邮件。在 15 分钟窗口内反复运行套件会触发它,此时失败会指向登录,而缺陷并不在那里。由此得出两个后果:浏览器每次执行只登录一次(一个setup项目保存状态;只有autenticacao.spec.ts例外,因为证明登录需要干净的源),并且suporte/api.ts识别这个 401 并说明它可能是什么。替代方案——要求 compose 为套件提供更宽松的阈值以便随意登录——被拒绝:它会为了让测试方便而削弱被测试的东西。
考虑过的替代方案
e2e/ 放在 pnpm workspace 内。 一次安装,没有第二个 lockfile。出于 ADR 0117 刚刚付出代价的理由被拒绝:产品的审计会带上 Playwright 的依赖树。
针对 vite dev 运行。 更快,没有要构建的镜像。被拒绝,因为它测试的是一个不被部署的拓扑,而同源的开发服务器会让 autenticacao.spec.ts 的每个断言都变得空洞。
专门的 E2E 作业。 更干净的分离,那里的失败不会报告在“images”之下。因成本被拒绝:它会为到达同一个 compose 而重新构建四个镜像。
通过向导驱动项目创建,而不是通过 HTTP 播种。 更真实,并会覆盖向导。暂时被拒绝——它会让执行时间长好几倍,并把这一层与一个 git provider 耦合,而这种覆盖属于向导自己的 spec,如果有一天需要的话。