验证¶
验证基线:cbb7b613(此分支在该文件之前的提交)。在 macOS 上使用 Node v22.23.1 执行(仓库在 .nvmrc 中固定了 24.18.0,但该版本未安装);AgentEnv 运行与实时对比是在一次性 Linux 虚拟机上完成的。以下每项检查均在最后一次源代码变更后按原样运行。
门禁¶
npm run build:cached、npm run check:fast(类型检查和 lint)、npm run format:check、npx knip、npm run scan:todos、npm run docs:check和npm run verify:test-discovery:通过。npm run jscpd:在 3538 个文件中重复行占 3.43%,低于 3.5% 的门槛。npx vitest run --project domain --project services:7407 个通过,5 个既有跳过。npm run test:prepare,然后对新的 write MCP 边界、serve MCP、CLI 和进程边界、export CLI 边界、analysis-view CLI 和 MCP 边界、compact 输入 schema 边界以及契约描述测试运行npm run test:focused --:10 个文件,72 个通过。npm run mcp-catalog:check:通过。
新测试:environmentStateWriteValues(94)、environmentStateWriteExplain(101)、environmentStateWriteApply(52)、environmentStateWrites(16,经视图的关卡)、environmentStateWriteAnswer(14,服务端)和 environmentServeWritesMcp(1,通过 MCP 的已构建 CLI)。由 serve MCP 边界固定的 replay 卡片随着响应摘要所携带的工具描述而变化;其校验和现在与合并后的分支匹配。
真实捕获¶
environment-state 视图在 50 个 showcase 捕获上运行(45 次 HAR 检查和 5 次 capture_browser_scenario 捕获),每次都在其预算内,耗时 8–110 毫秒。它在 16 个捕获中提供了 33 个写入工具(11 个创建、12 个更新、10 个删除)。在其余 150 个状态更改工具中,102 个没有指明状态类型或文档(大多数写入清单未模板化的字面单实体路径,或写入读取不受状态支持的集合),48 个已建模但未提供:
- 15 个未确认(后续读取未检查它们);
- 13 个无观察;
- 8 个相互矛盾,例如已删除的 Airbyte 数据源和 EspoCRM 任务仍然可读,Immich 和 BookStack 的条目读取显示出创建响应所没有的字段,以及 Sonarr 的 tag 列表排序;
- 7 个无法解释的变更,例如 BookStack 的 description_html 和 Paperless 派生的文本颜色;
- 4 个副作用,例如 Sonarr 的 tag 也会被
/tag/detail列出; - 1 个不完整实体:WordPress 对删除的响应中包含处于编辑上下文中的 post,而读取从未对种子 post 显示过这一点。
| 捕获 | 已提供(创建 / 更新 / 删除) | 未提供 |
|---|---|---|
| airbyte | 2 / 2 / 0 | 创建相互矛盾,删除相互矛盾,删除未确认 |
| baserow | 无 | 无状态目标 ×3 |
| bookstack | 0 / 0 / 1 | 创建相互矛盾,创建副作用,更新无法解释的变更 |
| directus | 无 | 无状态目标 ×2 |
| espocrm | 无 | 创建未确认,删除相互矛盾,更新无法解释的变更 |
| ghost | 无 | 无状态目标 ×2 |
| gitea | 1 / 1 / 0 | 无状态目标 ×2 |
| grafana | 无 | 创建未确认,无状态目标 ×5 |
| grist | 无 | 创建无观察,无状态目标 ×5 |
| grocy | 无 | 创建无观察,删除无观察,更新无观察,无状态目标 ×2 |
| healthchecks | 无 | 创建无观察,删除无观察,更新无观察,无状态目标 ×2 |
| homebox | 无 | 文档无观察,无状态目标 ×3 |
| immich | 0 / 0 / 1 | 创建相互矛盾,更新相互矛盾 |
| jellyfin | 无 | 无状态目标 ×2 |
| keycloak | 无 | 无状态目标 ×1 |
| komga | 无 | 创建未确认 ×2,无状态目标 ×4 |
| linkding | 2 / 2 / 2 | 创建未确认,删除未确认,更新未确认,无状态目标 ×2 |
| listmonk | 1 / 1 / 1 | — |
| lychee | 无 | 无状态目标 ×4 |
| mailpit | 无 | 文档无观察,无状态目标 ×5 |
| mattermost | 1 / 1 / 1 | 无状态目标 ×3 |
| mealie | 无 | 创建无观察、无状态目标 ×2 |
| memos | 无 | 无状态目标 ×3 |
| metabase | 0 / 0 / 1 | 创建副作用、更新未解释变更 |
| miniflux | 0 / 1 / 0 | 无状态目标 ×3 |
| n8n | 无 | 创建未确认、无状态目标 ×6 |
| navidrome | 无 | 创建无观察、无状态目标 ×2 |
| netbox | 1 / 1 / 1 | — |
| nocodb | 无 | 创建无观察、无状态目标 ×4 |
| nomad | 无 | 无状态目标 ×4 |
| paperless | 0 / 0 / 1 | 创建未确认、更新未解释变更、无状态目标 ×3 |
| photoprism | 1 / 1 / 0 | 删除未确认 |
| pinry | 无 | 创建未确认、删除未确认、更新未解释变更 |
| pocketbase | 无 | 无状态目标 ×4 |
| redpanda-console | 无 | 文档无观察、无状态目标 ×5 |
| sentry | 1 / 0 / 0 | 更新未解释变更、无状态目标 ×3 |
| shlink | 无 | 创建未确认、无状态目标 ×1 |
| sonarr | 0 / 1 / 0 | 创建矛盾、创建副作用、更新矛盾 |
| tandoor | 无 | 无状态目标 ×4 |
| umami | 1 / 1 / 1 | — |
| vault | 无 | 无状态目标 ×7 |
| vikunja | 无 | 无状态目标 ×4 |
| wordpress | 无 | 创建副作用、删除不完整实体、文档未确认、更新未解释变更 |
一次进程内冒烟测试推导出每个捕获对象的已服务状态,并用不同的请求体调用每个已服务的写入工具一次:全部 33 次调用均成功应用(状态与记录一致),并且每个已创建或已更新的实体都能从状态中读回;被删除的实体则返回了记录的 400 或 404(若有记录)。八个较早发布的清单不再匹配当前视图推导出的工具名称,因此它们的状态不可用,并按设计提供回放;其中没有一个具有已服务的写入工具。
在已发布环境上的端到端测试¶
五个已发布环境各提供创建、更新和删除一项(Linkding、Listmonk、Mattermost、NetBox、Umami);random.SystemRandom().choice 选中了 NetBox(NetBoxEnv-REA,NetBox 4.7.3)。
rea environment-export(由本分支打包)生成了该 bundle;其冒烟任务在 AgentEnv 框架 0.9.1334 中通过,共 42 个已服务工具(39 个读取工具,以及post_dcim_sites、patch_dcim_sites_by_id、delete_dcim_sites_by_id)。- 四个写入任务,每个任务由
env_outcome_verifier评分,该验证器通过/agentenvdata/get(而非智能体的回复)读取环境状态,并使用 replay-qa 智能体经由 Hugging Face 路由器,每个任务和模型各运行一次:
| 任务 | gpt-oss-120b | GLM-5.3-Flash | 工具调用(gpt-oss) |
|---|---|---|---|
| 在 Copper Valley 区域创建一个站点 | 1.0 | 1.0 | get_dcim_regions, post_dcim_sites |
| 更新预置站点的设施和描述 | 1.0 | 1.0 | get_dcim_sites, patch_dcim_sites_by_id |
| 删除一个预置站点且仅限于该站点 | 1.0 | 1.0 | get_dcim_sites, delete_dcim_sites_by_id |
| 创建一个站点,然后设置其设施 | 1.0 | 1.0 | get_dcim_regions, post_dcim_sites, patch_dcim_sites_by_id ×2 |
- 该 bundle 的 12 个检索任务(每个预置对象一个)在两个模型下均以 12/12 通过,与它们在已发布回放 bundle 上的表现一致。
服务模型与实时应用的对比¶
同一个 NetBox 4.7.3 镜像在 VM 上运行,由捕获对象自带的种子脚本进行预置(使用相同的 ids),并且同一场景同时针对实时 API 和由该捕获对象提供服务的环境运行。只有 created 和 last_updated 被规范化;其余每个成员都必须相等:
| 步骤 | 线上 | 环境 | 是否相等 |
|---|---|---|---|
| 列出站点 | 200 | 200 | 是 |
| 创建 Tide Gauge Annex(id 5) | 201 | 201 | 是 |
| 读取它 | 200 | 200 | 是 |
| 创建后列出(按名称排序) | 200 | 200 | 是 |
| PATCH 其描述和设施 | 200 | 200 | 是 |
| PATCH 后读取 | 200 | 200 | 是 |
| 创建 Granite Spur(id 6) | 201 | 201 | 是 |
| PATCH 一个预置站点 | 200 | 200 | 是 |
| 删除 Tide Gauge Annex | 204 | 204 | 是 |
| 读取已删除的站点 | 404 | 404 | 是 |
| 创建 Ledger Point(id 7,不复用 5) | 201 | 201 | 是 |
| 删除一个带有设备的预置站点 | 409 | 204 | 否 |
| 最终列表 | 200 | 200 | 否(由上面的删除导致) |
13 个步骤中有 11 个相等。差异在于 NetBox 的引用保护:它拒绝删除被设备引用的站点,而环境的删除操作只会单独移除该实体。捕获只删除了一个没有依赖项的临时站点,因此门禁无法看到这条规则;它被列在“未覆盖”部分下。
未覆盖¶
- 在 CI 中运行托管的
real-agentenv通道(此处的 AgentEnv 运行使用的是虚拟机上的已发布框架版本)。 - 真实捕获上的文档写入:四个已建模的写入未被服务。
- 与真实捕获上某次创建自身集合相关联的集合,以及通过 AgentEnv 运行得到的 UUID 或令牌身份(这两者都已由测试覆盖)。
- Agent 令牌用量与成本。
合并基础分支和 main 之后¶
该分支随后合并了 serving 分支于 5a61853a、coverage 分支于 b4b7ebc7(它们的评审修复)以及 main 于 edbd291a,然后又合并了 main 于 17f581ff(analysis-view 工具描述字符串,main 经常改动它)。上述门禁再次运行并通过:npm run jscpd 在 3579 个文件中重复率为 3.44%;domain 和 services 共 7442 个测试通过,其中 5 个预先存在的跳过;重点边界集合 72 个通过。50 个捕获的写入判定逐计划保持不变,进程内冒烟测试再次应用了全部 33 个已服务的写入工具。AgentEnv 运行和上面的线上比较在 cbb7b613 上运行,时间早于这些合并。第二次合并 main 之后:npm run check:fast、npm run format:check、npm run docs:check 和 npm run mcp-catalog:check 通过;contracts、analysis-view 和 environment 测试共 857 个通过,其中 3 个预先存在的跳过;analysis-view 和 environment 的 MCP 与 CLI 边界 53 个通过。
评审轮次之后¶
该分支随后合并了 serving 分支于 c00fde41、coverage 分支于 f6c756aa 和 main 于 1629266e,并采用了评审修复(82556cd5、138ea4b1、6d21b62e)。coverage 分支会在响应摘要中给字段名加引号,因此由 serve MCP 边界固定的 replay 卡片现在是 12,818 字节;去掉这些引号后,恰好恢复为此前的 12,618 字节摘要。门禁再次运行并通过:npm run check:fast、npm run format:check、npx knip、npm run scan:todos、npm run docs:check、npm run verify:test-discovery、npm run verify:module-boundaries 和 npm run mcp-catalog:check;npm run jscpd 在 3601 个文件中重复率为 3.47%;domain 和 services 共 7463 个测试通过,其中 5 个预先存在的跳过;重点边界集合 75 个通过;一致性测试 279 个通过。门禁更严格了,但 50 个捕获的写入判定逐计划保持不变,进程内冒烟测试再次应用了全部 33 个已服务的写入工具。AgentEnv 运行和上面的线上比较在 cbb7b613 上运行,时间早于这些合并和修复。
第二轮评审随后让门禁和 serving 在另外五个地方达成一致(读取按照 serving 匹配调用的方式与已检查的行匹配、每次调用的 increments 和 slugs 都被保留、读取某个条目后仍会学习插入规则、门禁中的文档正文检查,以及解码后的父路径值)。每个新测试在之前的 HEAD 上失败,在此处通过。相同的门禁再次通过(domain 和 services 共 7468 个测试通过,其中 5 个预先存在的跳过;重点边界集合 75;一致性测试 279;jscpd 3.47%),50 个捕获的写入判定再次逐计划保持不变,冒烟测试应用了全部 33 个已服务的写入工具。
第三轮保留了已检查行的路径拼写(replay 更倾向于逐字原样的拼写,因此同一父路径的两种拼写会保持为独立的两行),并将后续的 slug 规则与早期调用记录的值进行比较,而不仅仅是它们的成员名称;这两个新测试在之前的 HEAD 上失败。第四轮像 replay 一样选择某个读取的行,因此以普通拼写进行的读取会与回答它的编码行进行核对,其列表则与该行的集合核对;该测试在之前的 HEAD 上失败。
对构建在本分支之上的 fidelity 分支进行评审,又发现了四个不一致之处:另一路径上的文档写入现在在门禁中未建模;create 会选择调用中逐字拼写的集合;缺失实体会以记录的 2xx null 响应,而门禁过去将其接受为“不存在”;data/add 会拒绝其 data/get 形式会超过摄入限制的文档。每个新测试在之前的 HEAD 上失败。门禁再次通过(domain 和 services 共 7475 个测试通过,其中 5 个预先存在的跳过;边界 75;一致性测试;jscpd 3.46%),50 个捕获的判定逐计划保持不变,冒烟测试应用了全部 33 个已服务的写入工具。
第六轮在合并 serving 分支、coverage 分支和 main 335c042d 之后,又确定了五处发现:当读取的有效响应体不再包含实体或列表时,其检查会失败;实体缺少其视图始终携带的字段时,无法确认任何内容;被后续写入替换的写入不会通过读取替换结果得到确认;序列从不重用状态初始时已有的值;集合清空后仍保持其拼写。每个新测试在之前的 HEAD 上失败。在 50 个捕获中,有一个捕获的判定发生了变化:Linkding 的 bundle create 和 patch 之前仅通过同一 bundle 删除后的列表读取来确认,现在则未获确认,因此有 31 个写入工具被服务(10 个 create、11 个 update、10 个 delete),进程内冒烟测试应用全部 31 个。门禁再次通过(domain 和 services、边界、一致性测试和 jscpd)。
第七轮解决了另外两个发现。当后续写入仅覆盖更新所更改的一部分时,更新仍保留其确认状态;派生更改(例如递增的版本号)算作更新效果的一部分,后续递增是对该字段的追加而非替换。重置或加载会忘记被替换状态中集合的拼写。两个新测试在先前 HEAD 上均失败。50 个捕获的判定逐计划不变,进程内冒烟测试应用了全部 31 个已提供的写入工具,门禁再次通过。
第八轮解决了另外三个发现。后续写入会抹除它覆盖掉的先前效果的字段,因此两次写入若共同覆盖了更新的全部更改,更新便无任何可据以确认的内容。记录的结果会抹除先前写入在其实体上的更改。加载会保留 Evidence 自身的集合拼写,因此在加载状态不包含的集合中执行 create 会保留其记录的拼写。每个新测试在先前 HEAD 上均失败。50 个捕获的判定逐计划不变,冒烟测试应用了全部 31 个已提供的写入工具,门禁再次通过。
第九轮让 create 优先选择状态所持有的集合,而不是仅从 Evidence 得知的同样合适的拼写,因此加载后的 create 会落入加载到的集合,并能读回。其测试在先前 HEAD 上失败;判定与冒烟测试不变,门禁再次通过。
第十轮解决了 Greptile 在 list-lines 分支上针对本分支代码提出的六个发现:
- 对整份列表执行的无查询读取,会在提供应答的分页行上被检查;而查询重复了某个名称的行永远不会被检查;
- 在对其目标的写入仍在运行期间进行的读取,不检查任何内容;
- 后续调用若以计划之外的方式存储某个成员,则拒绝该计划;
- create 会保留捕获中列为空的集合的拼写;
- 大数组在检查依赖路径的值时不再导致栈溢出;
- 指南把已提供的写入工具计入目录的工具之列。
每个新测试在先前 HEAD 上均失败。50 个捕获的判定逐计划不变,冒烟测试应用了全部 31 个已提供的写入工具,门禁再次通过。
第十一轮解决了另外六个发现,其中五个在本分支上提出,一个在 fidelity 分支上提出:
- 通过的读取只确认那些它能显示出部分内容的写入;
- 条目读取像服务那样命名其实体,包括百分号解码后的拼写;
- 曾据以得知某写入效果的读取,仍然与其他写入相矛盾;
- 以某个值重复查询名称的行仍然可检查;
- 文档写入纳入效果跟踪,因此替换会覆盖先前的合并;
- 当调用命名的页面已超出该集合的范围,则从记录中应答(
page-exceeded),门禁不检查此类读取。
每个新测试在先前 HEAD 上均失败。50 个捕获的判定逐计划不变,冒烟测试应用了全部 31 个已提供的写入工具,门禁再次通过。
第十二轮解决了 Greptile 在 list-lines 分支上针对本分支代码提出的四个发现:
- 仅以其 id 应答的 create,只能通过读取该 id 才能被观察到;
- 后续调用若第一个在请求体中发送路径的 id,则把身份成员加入计划;
- 复制搜索使用索引遍历请求体,因此大请求体不再需要平方级时间(500,000 个零:之前 63 秒,之后 74 毫秒);
- 计划所保留的固定成员发送的值会计入视图的额度。
每个新测试在先前 HEAD 上均失败。50 个捕获的判定逐计划不变,冒烟测试应用了全部 31 个已提供的写入工具,门禁再次通过。
第十三轮解决了 Greptile 在本分支、fidelity 分支和 list-lines 分支上提出的七个发现:
- 通过的读取若未显示某写入的任何内容,则将该写入留待后续检查,而不是清除它;
- 在写入未知实体期间开始的读取,既不检查也不再次显示该实体;
- 后续 create 的序列字段若不符合已学得的序列,则拒绝该计划;
- 根节点以下的文档替换只覆盖它所替换的内容;
- 构建出的文档计划会将后续调用写入其自身的对象,即使该调用与文档不共享任何成员;
- update 或 delete 会像条目读取一样找到其实体,包括百分号解码后的拼写;
_start–_end范围在服务端和门禁中都像limit一样约束页面。
每个新测试在先前 HEAD 上均失败。50 个捕获的判定逐计划不变,冒烟测试应用了全部 31 个已提供的写入工具,门禁再次通过。
第十四轮解决了 Greptile 在 list-queries 分支上针对本分支代码提出的两个发现:
- 类型的身份字段永远不会变成常量:应用忽略了 id 成员的 update,不再把示例的 id 写入另一调用所命名的实体;
- 页面大小为 0 不限制任何内容,因此以
limit=0记录的整行会从状态应答。
两个测试在先前 HEAD 上均失败。50 个捕获的判定逐计划不变,冒烟测试应用了全部 31 个已提供的写入工具,门禁再次通过。
第十五轮解决了 Greptile 在本分支、fidelity 分支和 list-lines 分支上提出的九个发现,其中五个针对前几轮自身的改动:
- 名称看起来像指针的实体字段按名称匹配;只有文档的指针覆盖其下的成员;
- 失败的读取会与每个其后没有未建模写入的已执行写入相矛盾,而不是当最旧的待处理写入先于其中某个写入时跳过全部写入;
- 条目读取、update 或 delete 会像服务一样命名其实体,并只对 id 解码一次,因此
book%2531不再到达book1; - 若计划的调用将某个成员同时用作身份和固定成员,或以不同值将其固定,则该计划被拒绝;
- 大小为 0 仅在记录已在其下列出条目的行上不限制任何内容;
- 若 update 的观察结果丢弃了实体持有的某个字段,则该 update 被拒绝;
- 若后续 create 的时钟采用另一种格式,或其唯一值不是 UUID,则该 create 被拒绝;
- 若
data/get的派生状态会写入超过 32 MiB,则不会被提供。
每个新测试都在前一个 head 上失败。50 次捕获的判定结果逐计划保持不变,冒烟测试应用了全部 31 个已提供的写入工具,且门禁再次通过。
第十六轮解决了 Greptile 针对第十五轮改动提出的四个发现,涉及此处以及 fidelity、list-lines 和 list-queries 分支:
- size 为 0 并不单独构成边界:同一行上的 range 或其他 size 仍会限制页面;
- 仅当后续读取显示某个字段已被丢弃时,才会拒绝对该字段的更新;因为写入自身的响应可能不会包含某些字段;
- 干扰是针对工具在读取所涉实体或集合上的最新调用进行衡量的,而非其在任何位置的最新调用。
每个新测试都在前一个 head 上失败。50 次捕获的判定结果逐计划保持不变,冒烟测试应用了全部 31 个已提供的写入工具,且门禁再次通过。
第十七轮解决了 Greptile 在 list-queries 分支上针对本分支干扰规则提出的另外两个发现:当未建模的写入紧随该工具对读取所覆盖的任一目标的调用之后时,它会计入该工具的干扰;因此,之后对另一实体的调用并不能掩盖它;而一次失败的条目读取只会反驳那些没有被任何未建模写入跟随的写入,仅对其他写入在其成员上进行核查。两个测试都在前一个 head 上失败。50 次捕获的判定结果逐计划保持不变,冒烟测试应用了全部 31 个已提供的写入工具,且门禁再次通过。后续跟进仅在工具效果依然存续的目标上衡量干扰;因此,某次调用的效果若已被后续写入覆盖,便不再能为之后的不匹配开脱。该测试在前一个 head 上失败。
集合历史记账检查¶
2026-10-22,在 Node 24.18.0 上,我比较了写入历史变更(cfb42ddde)之前的源码与有界历史实现。每次操作都在一个新的集合路径下插入一个实体,该路径带有 128 字符的重复后缀,然后立即删除该实体。在五个全新的 Vitest worker 进程中,1000 个完整周期的中位数为:改动前 14.57 ms 墙钟 / 23,867 us CPU,改动后 9.98 ms / 16,914 us CPU。进程 RSS 采样在改动前为 381.1–387.9 MiB,改动后为 380.9–387.6 MiB。该 fixture 基准测试衡量的是单条匹配的已完成 trace,而非代表性用户任务或进程族的峰值内存。这些数值包含 Vitest 和 V8 的开销,且波动较大;它们并不能确定内存边界。1 MiB 策略是为额外标识符历史设置的一个确定性记账配额,其值为独立的 32 MiB 状态摄取上限的 1/32;它不是从 RSS 采样得出的,也不限制总存储或进程内存。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 rea2/rea