让 AI Agent 写 E2E 测试,最难的从来不是提示词,而是给它一只"手":浏览器控制、录制回放、页面快照、API 探测,背后都得有真实进程在干活。云端编排方案(SaaS job machine + 远程浏览器)延迟高、代码和页面数据还要过第三方;让 Agent 直接驱动本地 Playwright 又缺乏可控性和隔离。OpenEvident/vindicate 的解法是把运行时拆成三个本地进程:VS Code/Cursor 扩展负责 UI 与进程监管,runtime-mcp(MCP Server,端口 9223)是 Agent 唯一可见的工具面,runtime-worker(Fastify daemon,端口 9121)独占真实 Chromium。无云端组件、无 MongoDB、无远程编排,全部进程只监听 127.0.0.1。这套架构的核心矛盾只有一句话:如何让一台机器上的共享常驻进程,安全地为多个编辑器、多个项目的 Agent 并发服务。答案不是"每项目一个进程",而是"身份随请求走,不随进程走"。

三进程职责与拓扑

┌─ Agent / IDE ─────────────────────┐   ┌─ 本机 (127.0.0.1 only) ─────────────────┐
│ AI Agent (Cursor/Claude Code/…)    │   │  Vindicate 扩展 ──spawn + supervise────┐ │
│        │ MCP tool calls (HTTP+SSE) │   │      │ 读/写                           │ │
│   .cursor/mcp.json ────────────────┼──▶│  ~/.vindicate/worker.key (0600, 32B)  │ │
└────────────────────────────────────┘   │      │                                │ │
                                         │  runtime-mcp  :9223  POST /mcp        │ │
                                         │      │ HTTP + x-vindicate-internal-key│ │
                                         │  runtime-worker :9121 (Fastify)       │ │
                                         │      │ Playwright CDP                  │ │
                                         │  Chromium                             │ │
                                         └───────────────────────────────────────┘ │
组件 职责 关键点
Vindicate 扩展 引导 UI、仪表盘、进程监管 本身不做自动化;WorkerManager/McpManager 继承同一个 BaseProcessManager,由 RuntimeLifecycle 协调启停
runtime-worker 真实浏览器交互 Fastify + SSE;会话状态机 active → paused → dead → expired → closed;录制产物落到 <project_root>/.vindicate/recordings/
runtime-mcp Agent 的工具面 @modelcontextprotocol/sdk 的 StreamableHTTP 传输;进程无状态,磁盘文件是唯一事实来源(files-as-truth)

扩展激活即无条件拉起 worker(有工作区再拉起 mcp),切换工作区只重启 mcp、不碰共享 worker——这是"单例"模型的直接后果:worker 和 mcp 是机器级单例,同一台机器上 Cursor 和 VS Code 各开两个项目,四个编辑器窗口共享同一个 9121 和同一个 9223 进程。

关键设计一:单例进程如何做到多项目隔离

正确性完全依赖"project_root 随请求/会话走"。扩展给每个项目写 MCP 配置时,把绝对路径烤进 URL:

{
  "mcpServers": {
    "Vindicate": {
      "url": "http://127.0.0.1:9223/mcp?project_root=/Users/me/work/app-a",
      "headers": { "x-vindicate-project-root": "/Users/me/work/app-a" }
    }
  }
}

runtime-mcp 为每个 MCP 客户端连接新建一个 McpServer + ProjectFs 实例,绑定该会话的 project_root,所有文件读写、codegen、测试执行都限定在这个目录。worker 侧同理:POST /sessions 创建会话时把 project_root 印在会话记录上,之后该会话的一切文件操作(/sessions/:id/files/*)都从会话记录读回路径,而不是读进程级配置。Agent 永远看不到、也设置不了 project_root——它不是工具参数,是扩展写配置时注入的连接级属性。一个 worker 进程同时服务多个项目的会话,靠的就是这条铁律。

关键设计二:内部密钥认证——防的不是人,是浏览器标签页

系统没有任何用户体系:无云账号、无登录。唯一认证机制是 32 字节随机值,一次性生成写入 ~/.vindicate/worker.key(0600 权限 + 原子创建,防止两个编辑器同时首启时竞态生成出两个 key),所有对 worker 的请求带 x-vindicate-internal-key 头。它只证明"请求来自本机自己的 Vindicate 安装",不携带任何用户身份。威胁模型很具体:本地 9121 端口是个能驱动浏览器的自动化 daemon,任意网页 JS 都能向 localhost 发请求——没有这个头,浏览器里开个恶意页面就能 CSRF 你的测试机。注意 MCP 的 /mcp 端点本身不 gate(Agent 要连),gate 的是 worker HTTP API 和 mcp→worker 这条链路。

关键设计三:用工作流图替代巨型 system prompt

vindicate_workflow 工具下发的是内容图而非一坨提示词:content/graphs/*.graph.json 定义节点和边,content/nodes/*.md 是各阶段的指导正文(YAML frontmatter 声明所属 graph、可用模式、要追加哪些 refs),content/refs/*.md 是共享参考资料(conformance contract、POM 约定、配方),多个节点复用同一份 ref,避免复制粘贴导致的事实漂移。主图是一条完整的测试自动化闭环:

understand → ground → design → generate → execute → audit
                              ↑        ↑        ├→ heal → ground/generate
                              └────────┴────────┴→ escalate

入口按任务路由:writefixflakyrefactorgapscoveragesmoke 各自指向对应起始节点。ground 阶段还会做 api-ingest:抓真实端点形状,为后续 API 模式 codegen 提供契约。

关键设计四:codegen 直接写盘,两条轨道

vindicate_generate_code 不搞 schema 往返,直接把生成的代码写进项目。UI 轨道从 browser_read 拿到的定位器生成 page object / spec(validatecreateadd_test_casesregister_page 四种模式);API 轨道从 ground 阶段的端点形状生成 resource client、builder、expected.json 和 auth fixture(validate_apicreate_apiadd_api_test_casesregister_client)。mcp 与 worker 之间的共享契约放在 monorepo 的 packages/protocol/,用 Zod schema 定义,两端编译期即对齐——这比"两边各写一份 TypeScript 接口"的常规做法省掉一整类"协议漂移" bug。

一次录制任务的完整数据流

把上面四个设计串起来看一次 record 任务:Agent 调 vindicate_start_recording(工具面在 mcp 进程)→ mcp 用内部密钥向 worker POST /sessions 建会话(状态 active,记录里印上 project_root)→ worker 拉起 Playwright + Chromium(CDP 连接)→ 录制产物经 /sessions/:id/files/recordings/xxx.jsonl 落盘到 <project_root>/.vindicate/recordings/ → Agent 调 vindicate_stop_recording 拿到产物路径 → 调 vindicate_generate_code 让 mcp 读取产物并生成 POM/spec 直接写回项目目录 → 调 vindicate_run_tests 让 worker 在项目里执行 npx playwright test。整个链路里 Agent 从未直接触碰文件系统 API,所有 IO 都收敛在 mcp 的工具定义里,而 mcp 自己又不持有任何状态——状态在 worker 的会话记录里,事实在磁盘上。会话记录同样持久化:worker 重启后,TTL 内的会话(默认 30 分钟)从磁盘恢复,Agent 可以继续 resume 而不是推倒重来;会话状态机里的 dead/expired/closed 三个终止态把"进程崩溃""超时"和"正常关闭"区分开,日志里一眼能看出是哪种收场。

什么时候这套架构是过度设计

单例 worker + 密钥文件这套东西的复杂度,是为"多编辑器、多项目、常驻浏览器"买单的。如果只是自己本地偶尔让 Claude 写两个测试,直接给 Agent 一个 npx playwright test 的工具就够了;如果团队要的是多人共享的测试执行环境,本地单例模型反而不合适——它刻意不做并发队列、不做远程访问,多人协作场景应该上 CI 或专门的 runner。vindicate 的取舍很清晰:它服务的是"个人开发机上,Agent 与浏览器之间最短的那条本地回路",一切设计(单例、密钥、空闲自关闭、files-as-truth)都围绕这个场景优化,不为别的场景预留抽象。照搬架构前先确认自己的场景和它一致。

踩坑记录与工程细节

  • per-editor 随机密钥冲突:早期每个编辑器在各自的 secret storage 里生成独立 key,第二个编辑器打开时必然撞上"worker 已被不同 key 占用"。改成共享 key 文件后,多编辑器、多 profile、多应用都能 attach 到已在运行的 worker。升级后需彻底退出编辑器一次(Reload Window 不够)清掉旧进程。
  • 进程静默死亡:最常见的线上崩溃原因是 unhandled rejection 直接带走进程。现在两个进程都装了 unhandledRejection/uncaughtException handler,日志后保活。注意系统没有自动重启——崩溃后靠扩展每 30s 的 GET /health 探活显示为 down,需要用户重载编辑器窗口触发 respawn。
  • 双进程同时首启的竞态:两个编辑器同时 spawn 时,输家检测到自己 spawn 失败后重新 probe 并 attach 到赢家,而不是报错。
  • 客户端兼容性坑:Antigravity 的 MCP 配置不能带 headers 字段(google-antigravity/antigravity-cli#71,带了会让所有工具调用失败);Claude Desktop 因为没有 workspace 概念、不符合 per-session project_root 模型,直接从 onboarding 移除。
  • 空闲自关闭:单例 worker 没人替它收尸——最后一个编辑器关闭后,连续 5 分钟(VINDICATE_IDLE_SHUTDOWN_MS)无 health ping 且无活跃/暂停会话就自行退出。
  • 资源治理:worker 采样 CPU/内存,压力大时节流或拒绝新会话;browser_diagnose(视觉诊断)用 VINDICATE_VISUAL_DIAGNOSIS 环境变量做全局 kill switch。
  • 多 root workspace:只取第一个文件夹,多个时打警告日志。

安全基线与端口协议

组件 端口 协议 认证
runtime-worker 9121 HTTP + SSE (Fastify) x-vindicate-internal-key 头(除 /health、/ready 外全部路由)
runtime-mcp 9223 MCP Streamable HTTP (POST /mcp) /mcp 不 gate;自身调 worker 时带内部 key

所有端口只绑 127.0.0.1,整个系统不存在任何网络面。worker.key 就是这台机器的信任锚:谁拿到它谁就能驱动本地浏览器,所以权限 0600、原子创建、不入库。

总结与进阶方向

vindicate 给"AI 写 E2E 测试"提供了一个可复用的架构模板:扩展做进程生命周期、MCP 做工具面、Worker 做浏览器执行,三者用固定端口 + 密钥文件解耦;多项目隔离靠请求级身份而非进程级隔离。对自建类似工具的人,三个可直接抄的点:① 工作流指导用 graph/nodes/refs 三层结构,别堆 system prompt;② 共享契约用 Zod schema 放独立 package,两端同源;③ 本地 daemon 必须做 CSRF 防护,哪怕只是给 localhost 端口加个共享密钥头。

进阶方向:Agent skill 标准化(.agents/skills/ 共享路径,Cursor/Copilot/Antigravity 三端复用一份)、把录制回放升级为"录一次、断言自动生成"、以及把单机 worker 抽象成可插拔远程执行器——但按这个项目的取舍,后者大概率会被按需才加,本地优先是它的立身之本。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐