为 Next.js 项目搭建 instant-navigation 测试基线:读懂 rig 六问与 `instant-nav.rig.md` 生成指南
在 Next.js 的 Cache Components / PPR(Partial Prerendering)优化实践中,"让路由导航即时可见(instant navigation)"是可以通过自动化测试严格验证的,而验证的前提是为你的具体项目搭好一套 rig(运行基线)。rig-template.md 正是这一环节的脚手架:它先把"原理与环境无关、基建与项目强相关"这件事讲清楚,再给出一次性的勘探(discovery)流程、六个必答问题、两个派生字段、可直接套用的 instant-nav.rig.md 文件模板,以及三种真实场景的填写范例。读完本文,你将掌握如何为任意 Next.js 项目产出自己的 rig 文件,让后续每次 RED/GREEN 验证都跑在可信的生产构建上。
一次勘探,终身复用:rig 的定位与产出物
本仓库的 next-cache-components-optimizer 技能提出了一个清晰的分工:原则(invariant)是固定的,基建(rig)是你的。原则只要求"验证循环"必须存在且可信,但"怎么构建、怎么部署、怎么鉴权、怎么跑 Playwright、怎么循环"完全取决于你所在的技术栈。
- 本地
next build && next start、CI/暂存环境的容器、每次 push 产生的 preview 部署,三者在原则层面等价; - 结论永远来自"被测的构建产物",而不是来自"谁帮你跑了它"。
rig-template 文档把这件事落成一步到位的操作:对每个仓库只做一次勘探,把答案写进一个随仓库提交的 instant-nav.rig.md(放在仓库根目录,或放在 e2e 配置旁边),此后每次运行直接读该文件,不再重复勘探。技能里把这一步编排为 Phase 0(SETUP),在整个工作流(P 前置检查 → 0 勘探 → A 搭建 rig → B 基线 → C RED → D 修复 → E 对等性 → F 差分 → G 评审)中,SKILL.md 明确指出 rig 文件是"一次性搭建、每次优化可被构造性验证"的基础设施,而不是某一单个路由的产物。
勘探方法论:先查仓库,再问用户
rig-template 对勘探方式有明确主张:先检查,再提问(Inspect before asking)。绝大多数答案已经写在仓库里,按顺序排查以下五类来源即可:
package.json的 scripts(重点找build、start、test:e2e);- e2e 配置(
playwright.config.*中的baseURL、webServer、projects); - CI 配置(
.github/workflows/、vercel.json、GitLab/CircleCI 配置、Dockerfile); next.config.*(是否已有experimental标志、当前的cacheComponents/ PPR 开启状态);- 现有 e2e 鉴权助手(在仓库中搜索
login、storageState、session等关键词)。
只有仓库回答不了的问题才需要问用户,典型的是三类:哪个部署目标算"preview"、CI 中测试套件以哪个账号运行、以及 Agent 是否被允许在无人值守情况下推送并等待 CI。其余一切应以仓库证据为准。
六个必答问题 + 两个派生字段
rig 文件要求六问全部有答案,另外还有两个由勘探过程"喂"出来而非直接提问的派生字段:LIVENESS(由 LOOP 答案推导的、回显 SHA 的探针)与 WALLS(针对项目特有构建/运行障碍的积累清单)。
1. BUILD:生产构建如何产出与被服务
定义"被测的生产构建"来自哪里:按 push 触发的 preview 部署、暂存容器,还是裸的 next build && next start。除了 next dev,任何一种都算——这正是 rig 的第一个硬性约束(参见 SKILL.md 的 Phase A):next dev 不做预取、其锁对阻塞路由不可靠,因此 dev 环境下的 instant() 结果不能当作有效的 RED 或 GREEN。
2. EXPOSE:何时打开测试 API,且永远不用于真实生产
experimental.exposeTestingApiInProductionBuild 是让生产构建内嵌测试 API 的开关,rig 必须为"每一个被测构建"打开它、并为"真实生产"永远保持关闭。常见写法有三种,rig 文件需记录本项目采用的那一种:
- 显式环境变量:
EXPOSE_TESTING_API=1(适合本地生产构建); - 通用 CI/暂存变量:
process.env.DEPLOY_ENV === 'staging'; - Vercel 平台条件:
process.env.VERCEL_ENV === 'preview'。
关键是条件必须在 next build 时成立,而不是只在 next start 时成立——否则 instant() 可能无法在测试超时前拿到测试 cookie,先重建产物再调试断言,而不是去调试断言本身。
这一开关并非 rig-template 的发明。在 Next.js 源码中,packages/next/src/server/config-shared.ts 给出了它的官方注释:该标志将 "Instant Navigation Testing API" 暴露到生产构建(该 API 在开发模式下始终可用),使 e2e 测试能够控制导航时序、在动态数据流入前对预取/缓存 UI 做确定性断言,并明确警告"该标志仅供性能剖析与测试使用,切勿在面向用户的生产部署中启用"。这就是文档反复强调 EXPOSE 条件必须与真实生产互斥的底层依据。
3. RUN:Playwright 套件如何被调用、针对哪个 BASE_URL
记录 e2e 的实际执行命令与目标地址来源:是 BASE_URL=http://localhost:3000 playwright test 还是 CI job 中的其他拼写。BUILD、EXPOSE、RUN 三问共同回答"在哪个产物上、以什么条件、跑哪条命令"。
4. TEST USER:套件以哪个账号运行、登录如何发生
套件(在 CI 中即 CI 账号,本地即 e2e 登录 fixture 所代表的账号)以何种方式鉴权——helper、storageState 还是 API token?该账号持有哪些 flags / plan / role / data?答案直接喂给 Phase B 的"以测试用户身份确认 marker 真实可达"与 C-gate 的可信度判断。
5. DRIFT:作者会话与测试用户环境之间的一切差异
把可能造成差异的每一项都枚举出来:功能开关、套餐与授权(entitlements)、角色、种子数据与空数据、locale、A/B 分桶。每一项都是 RED 可能变得不可信(untrustworthy)的通道,这份清单是 C-gate(参考 reference/red-test-robustness.md)的输入:作者自己的登录会话里能看到 marker,不代表测试账号能看到。SKILL.md Phase B 也点名了这一点——套件以测试账号运行,而不是以作者会话运行,环境漂移是产生不可信 RED 的常见根源。
6. LOOP:无人值守的迭代闭环
给出你 rig 的闭环:推送 → 构建 → 对产物跑 e2e → 读失败 → 修复 → 再推送(CI 路径);或构建 → 启动 → e2e(本地路径)。同时记录 Agent 无法独立完成的事项(部署审批、密钥、受保护分支)。LOOP 还要求包含活性探针(liveness probe):
- 找到能回显被部署提交 SHA 的端点或响应头(例如
/healthz路由或x-deployed-sha头),让 CI 在相信某个判定前先确认"被测构建 == HEAD"; - 若平台不暴露任何回显 SHA 的端点/响应头,就自己加一个:把构建期提交变量(
VERCEL_GIT_COMMIT_SHA、CI commit 变量)挂到/healthz或某个响应头上;或退而轮询部署平台 API,找出commitSha === HEAD的那次部署; - 记录所选机制。
本地 build && start rig 无需 SHA 探针——被测产物就是刚构建出来的那个。但本地闭环仍有三个硬性注意事项:记录端口、启动前停掉上一次的服务、端口占用时让闭环失败(EADDRINUSE);测试前确认监听该端口的是新启动的进程。由于 next start 会 fork 出 next-server 子进程,启动器进程 ID 不一定持有端口,因此要么把服务放进一个能被 rig 整体停掉的进程组,要么在下一次构建前发现并停掉占用记录端口的进程。
产出物:instant-nav.rig.md 文件模板
勘探完成后,复制以下模板、逐字段填写、随仓库提交:
# instant-nav rig: <project>
- BUILD: <command / platform that produces the measured production build>
- EXPOSE: <the condition wired to exposeTestingApiInProductionBuild>
- RUN: <e2e command> against <how BASE_URL is obtained>
- TEST USER: <account> via <login mechanism>; flags/plan/role/data: <...>
- DRIFT: <the enumerated drift surface>
- LOOP: <push → CI → e2e, or local build → start → test>; agent limits: <...>
- LIVENESS: <endpoint/header echoing the deployed SHA; n/a for local build && start>
- WALLS: <project-specific build/run obstacles + their workarounds>
WALLS:撞过的墙,都要记录下来
真实应用几乎不可能第一次就为生产顺利构建:缺密钥、导致预渲染失败的 server-only 导入、被反复重启的服务占用的端口……rig-template 明确要求第一次撞上某堵墙时就记下这堵墙及其绕过方式。WALLS 是其他字段无法捕获的、项目特有的构建/运行障碍的积累地,也是把"勘探"从一次性动作变成可持续资产的关键。
三种真实 rig 的填写范例
rig-template 给了三个可直接对照的填好的例子,分别覆盖无 CI、通用 CI+容器、Vercel preview 三种主流形态。
无 CI / 纯本地
- BUILD:
EXPOSE_TESTING_API=1 next build && next start - EXPOSE:即该环境变量
- RUN:
BASE_URL=http://localhost:3000 playwright test - LOOP:单机完成 构建 → 启动 → 测试;全程可被 Agent 独立驱动——无需推送、无密钥、无部署等待。
通用 CI + 容器
- BUILD:流水线构建镜像并部署到暂存 namespace
- EXPOSE:
process.env.DEPLOY_ENV === 'staging' - RUN:CI job 对暂存 URL 运行 Playwright
- LOOP:推送 → 流水线 → e2e;流水线就绪后即可全程无人值守。
Vercel preview 部署
- BUILD:每次 push 构建一个 preview
- EXPOSE:
process.env.VERCEL_ENV === 'preview' - RUN:
playwright test,BASE_URL=<preview URL> - LOOP:推送 → preview → e2e;preview 部署与
VERCEL_ENV门控就绪后即可全程无人值守。
对照你的项目选一条主线即可,同时注意 rig 文档的态度:平台、环境变量拼写、命令都只是"待翻译的示例"而非"必须遵守的要求"——把原理翻译成你仓库的真实形态,而不是反过来改造仓库去迎合示例。
为什么 rig 必须在"锁"上跑:从机制到验证闭环
理解 rig 的每个字段,最终都要回到 @next/playwright 的 instant() 机制上,否则容易在 EXPOSE、LIVENESS 等字段上踩空。在 packages/next-playwright/src/index.ts 的 instant() 实现中可以看到完整协议:它通过浏览器上下文 addCookies() 写入名为 next-instant-navigation-testing 的 cookie(值形如 [0, p<random>],见同文件第 30 行常量定义),cookie 触发构建内的 CookieStore change 事件从而获取内存中的导航锁;在锁内,Next.js 只提供缓存/静态外壳数据,动态数据被门控;回调完成后按过去过期时间(expires: 1)重写 cookie 来释放锁。
由此可以推导出 rig 的几条硬约束,它们恰好对应 rig-template 反复强调的要点:
- 空转通过(vacuous pass)是最危险的失败模式。如果构建产物没有内嵌测试 API(即 EXPOSE 没配好),cookie 会被忽略,
instant()不抛错、导航正常执行,测试"静默通过"。正如 reference/red-test-robustness.md 所述,绿色instant()测试只有在锁确实生效时才有意义,而验证锁生效的最直接证据就是 Phase C 的 RED 本身。 - 基线的先决条件来自 EXPOSE 而不是 RUN 阶段:锁必须在
next build期间就嵌入产物。文档明确提醒:instant()获取测试 cookie 失败导致超时,通常是 EXPOSE 条件只在next start时成立、构建时未内嵌 API 所致。 - 远端 rig 必须先验活再信判定:对任何部署/远端构建,先轮询 LIVENESS 探针确认产物包含
HEAD,否则过期部署会读出假的 RED 或 GREEN。本地next build && next start无需此步。
把这些机制与 test-template.md 中"软导航用真实 <Link> 点击、硬导航用 page.goto() + baseURL"的测试模板对照,可以串出整条链:rig(本模板产出)保证"在哪个可信构建上测";测试模板保证"测什么断言";reference/red-test-robustness.md 保证"RED 为何红、是否可信"。三者共同构成可构造性验证:最大化静态外壳的价值取决于能否证明它,而证明方式就是 rig 支撑下的自动化检查。
小结:rig 是交付物,不是一次性杂务
如果你只记得一句话,那就是:rig-template 教你把"验证循环"固化成一个随仓库提交的 instant-nav.rig.md,让每一次"该路由是否 instant"的判定都有同一个可信起点。六问(BUILD / EXPOSE / RUN / TEST USER / DRIFT / LOOP)把项目基建翻译成可重复的答案,LIVENESS 与 WALLS 把"部署对没对上 HEAD""踩过哪些坑"沉淀成资产,三个填充范例则给了从纯本地到 CI 到 Vercel preview 的起点模板。从 packages/next/src/server/config-shared.ts 的配置开关到 packages/next-playwright/src/index.ts 的锁协议,仓库源码完整支撑了模板里每一条看似武断的规则——它们不是风格偏好,而是让验证不空转、让绿色有意义的前提。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00