Agent 自证清白。
DOM 第一次
长出⟨机器可读表面⟩。
Contract testing 不是新概念。Fixtures、invariants、合约—— 这些 pattern 在教科书里活了二十年, 但只有少数后端团队真的把它跑成日常。 AI agent 第一次让它在前端经济上成立—— 代价从「人写、人维护、人解读」翻成趋近于零的边际成本。
这不是一项技术发明,是经济学翻转带来的实践回归。 Anthropic 真正贡献的,是为「agent 与前端产物之间的协议」补上了一份具体的参考实现。
DOM 直接发射状态
每个组件在最外层元素上挂 data-verify-*,把内部状态当作合约暴露在 HTML 表面。Agent 不再需要去推导渲染结果,直接读属性即可。
声明绑定组件
Fixtures + invariants + probes 写在 .verify.ts 里,与组件同框。验证矩阵就是组件的活文档,agent 改代码时一并维护。
三个消费者一份数据
人工 Dashboard、agent __verify.runAll()、CI vitest 矩阵——共用同一个 runFixture() 代码路径,没有版本漂移空间。
谈论"AI 让验证变可行"之前,必须先承认一件事:在大多数前端团队里,"严格验证"从来就没真的在场。把 contract testing 当作"原本就在做的事"是最常见的误读。
把 2024–2025 年几份覆盖面较大的调查并到一起,画面相当一致——单元测试稀薄、E2E 几乎缺席、覆盖率长期低于人们口头承认的水平,而做得起的团队也把维护成本列为最大痛点。
Contract testing 这个概念早就存在。后端微服务之间的契约校验、Pact 等工具都活跃了多年。问题不在"想不到",而在于价值和成本的兑现窗口错位——价值要到部署之后才被感知(避免线上 bug),成本要在开发当下就被预付(写 fixture、维护 invariant、排查 flaky test)。
这套"先付账后兑现"的经济学,使得 contract testing 在 UI 层尤其难落地。看几位前线工程师怎么说:
The biggest issue seems to be setup and maintenance—it takes effort, and if teams on both sides of the contract aren't fully committed, it quickly becomes useless.
Some folks found it so cumbersome that it never even caught meaningful bugs, making it feel like a wasted investment.
At the last client I worked at, it took me almost a year of my "free time" at work to get one suite of flakey full stack Cucumber tests running reliably!
API contracts are formal, automated, and reassuring. When they pass, pipelines turn green and deployments feel justified. The problem is that contracts answer a very specific question, and it's not the one users care about.
把这几条放一起看,结论很朴素:在人力时代,验证基础设施不是"可不可行",而是"养不养得起"。能养起的团队不多,就算养了,也有相当一部分会被维护成本反噬。
The more capable the models get, the more you should try to resist constraining them.
Ara · Code with Claude London 2026 · Workshop "How We Claude Code"
传统上,agent 想知道一个组件"现在到底是什么状态",必须读它生成出来的 HTML/CSS/文字 reverse-engineer 状态。这是个二阶推理问题:渲染层把 props 翻成 DOM,agent 又要把 DOM 翻回成"语义状态"。中间两次有损翻译。
Anthropic 的做法是:让组件把自己的语义状态直接发射到 DOM 属性上。`data-verify-unit="TodoApp"`、`data-verify-total="3"`、`data-verify-done="1"` ——这些不是渲染副产物,是显式合约。
data-verify-unit="TodoApp"
data-verify-total="3"
data-verify-done="1"
data-verify-active="2"
data-verify-filter="all">
…
</section>
section 不仅渲染了 todo 列表,还在外层属性上把"我现在内部认为的状态"原样贴出来。Agent 只需 query DOM 即可。但这跟 data-testid 不一样吗?——形式上像,意图完全不同。data-testid 仅仅给元素一个标识,回答"我是谁";data-verify-* 暴露完整状态契约,回答"我是谁 + 我现在的状态是什么"。一个是定位器,一个是合约。
.verify.ts)__verify.runAll()把 verifyAttrs() 拆开看,它干的事情就是遍历对象、加上 data-verify- 前缀、把所有值字符串化。仅此而已。它本身没什么聪明的,聪明的是这个约定本身。
就这么短。价值不在代码量,在于这是整个 codebase 都遵守的约定——一个 6 行约定取代了上百行 testing utility。
每个组件加 1 行—— spread 到外层元素上。剩下的事 React 自动接管:每次 render,最新状态自动发射到 data-verify-*。
验证结果的"唯一真源"是一个叫 runFixture() 的函数。它接受一个 unit + fixture,返回 PASS | FAIL | BLOCKED | SKIP 四种判定。三个使用者各自调用同一个函数,分别得到自己需要的形态:
"unit": "TodoStats",
"fixture": "mixed",
"verdict": "PASS",
"checks": [
{"id":"schema","ok":true},
{"id":"invariants","ok":true}
]
}
await __verify.runAll() — 给 agent 自动决策的结构化 JSON。✓ TodoStats > none-done
✓ TodoStats > all-done
✓ 🔍 inconsistent-counts
→ FAIL (by design)
✓ TodoInput > typed
✓ TodoList > populated
21 fixtures · all expected verdicts
bun run verify — vitest 矩阵跑全量,CI 红绿灯。三个 surface 共享一份代码,意味着它们之间没有版本漂移空间——dashboard 看到的不会跟 CI 跑出来的不一致。这是个反"集成地狱"的设计:你不可能让其中一个先过,另一个后挂。
Workshop 配套的 cwc-workshops/how-we-claude-code 包含 5 个 verifiable units 与 21 个 fixtures。clone 下来跑 bun run verify,输出大致如下:
inconsistent-counts 的 FAIL:
这是一个 probe——一个故意违反 invariant 的 fixture(total=10, done=3, active=4,但 3+4 ≠ 10)。
测试矩阵在 EXPECTED_FAIL 集合中显式断言它必须 FAIL。
probe 存在的意义是:验证验证系统本身没在睁眼说瞎话。所有都 PASS 不该让人放心,应该让人怀疑。
把每个环节拆开看人力成本和 agent 成本,会得到一张相当鲜明的对照表。读这张表的方式不是"AI 替代了人",而是原本因为太贵而被舍弃的那一段,现在重新进入预算。
关键转变是这一条:验证从"一次性投资(写完之后很少更新)"变成"持续维护的活文档"。前者在人力时代是合理的取舍——因为持续维护不经济;后者在 agent 时代是自然的——因为持续维护已经不需要持续付钱。
到这里我们已经讲了三个:DOM 表面、verifyAttrs、三个消费者。把六个一起列出来,方便实操对照——它们彼此互锁,缺一个就漏掉一个能力。
data-verify-*,把内部状态作为合约暴露在 HTML 上。.verify.ts 文件:Zod schema、命名 fixtures、不变量谓词、probes。/verify/:unit/:fixture。无 app shell,可单独驱动。The agents are going to be doing more and more of this natively, and how can you set the artifacts that you produce up to natively be testable and verifiable in the way that you need.
Ara · Code with Claude London 2026
Repo 还附了一个 scripts/record.ts,启动 headed Chromium、导航到 /verify/replay、等 window.__verify_replay.done === true,然后把整段验证过程录成 .webm 视频存到 recordings/。
这个脚本本身不奇特——奇特的是它构成的工作流:开发者改完代码,agent 自动跑 verify、自动录视频,PR 里直接附上录屏。Reviewer 不用本地跑、不用打开浏览器,看一眼录屏就能确认行为是否正确。
The Claude Code team records basically all the code changes that they do like this, all the front end changes at least, especially at the pace of shipping that we have at the moment.
Ara · 描述 Anthropic 内部工作流
报告本身不下任何"普适"判断。直接列出 demo 自带的、肉眼可见的局限:
data-verify-*,需要别的合约协议。verifyAttrs() 调用(每个组件约一行)。它不是零侵入——是侵入但廉价。Contract testing 本来就在那。data-attribute 也本来就在那。React 把状态翻成 DOM 这件事更是讲了十年。Anthropic 没有发明任何一项新技术——他们做的是把这几件早已存在的东西串成一个 agent 友好的协议,并提供了一份能跑起来的参考实现。
这件事的价值不在"todo app demo 跑起来了",而在于:当 agent 写代码成为常态,"agent 能不能验证它写的东西"就不再是可选项,而是基础设施。这份 demo 给"agent 与前端产物之间的契约"画了第一根线——不一定就是最后那根线,但它把空间打开了。
剩下的事,是行业能不能也跟上来——能不能在框架层、设计系统层把 data-verify-* 和 __verify 变成默认能力,而不是每个团队各写一套。
- 验证不再是"人写测试"。它是"agent 自证"。组件渲染时把状态发射到 DOM,agent 读属性即可。
- 同一份验证逻辑撑起三个 surface。Dashboard、Agent API、CI vitest——共享
runFixture(),没有漂移空间。 - probe 是验证体系的免疫系统。故意失败的 fixture 证明系统没在睁眼说瞎话。所有都 PASS 该让人怀疑。
- "AI 解决了所有测试问题"。它没有。视觉正确性、性能回归、跨浏览器一致性——这些都不在
data-verify-*覆盖范围内。 - "这是行业标准"。不是。是 Anthropic 一家的内部实践 + 一个 reference repo,且未经社区采纳验证。
- "零侵入"。每个组件需要主动 spread
verifyAttrs(),每个 unit 需要写.verify.ts。代价低但非零。