test(e2e): Agent 行为全栈测试体系(serve 37 + hub 拓扑 4 场景)+ 入 CI 门禁#206
Merged
Conversation
多 agent 进程因共享故障同时重试时,纯指数回退会 lockstep 收敛成同步风暴。仅对默认指数回退加 ±20% 抖动;Retry-After 服务端指令仍按原样尊重。抽出纯函数 jittered_wait_ms + splitmix 熵,jitter 带宽可单测。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
provider/代理可能停止 yield 却不关闭 socket,此前整轮只能干等 reqwest 的 300s 总超时。给流消费循环的 stream.next() 套 90s idle 超时,超时按 truncation 处理并复用既有 auto-continue 恢复路径。test-support 新增 StallThenProvider,配 start_paused 虚拟时间零等待验证恢复。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Edit 的 no-match 从死消息 "old_string not found" 升级为 edit-core::no_match_hint 生成的可执行提示:诊断仅差 CRLF/空白的一步可修情形、定位文件中最接近的行、并提示重读可能已变动的内容。MultiEdit 追加针对其顺序语义的重读提示。贯彻 stage4「错误即模型可执行信号」教条。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
此前 memory_nodes.body_preview 仅存前 300 字节、FTS 只索引这段——长笔记正文对关键词检索不可见(近乎 bug)。规范化为:节点存完整 body(源自 .md),FTS 索引全文;preview 降为展示期投影(recall 输出截断 280 字符),synthesize 改用全文。schema bump v2 + 派生 DB 版本不匹配时 drop 重建(下次扫描从 .md 源重填)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
多进程下多个 agent 可能共享 cwd,「读→(他方改)→盲写覆盖」是真实竞态而非理论问题。新增 per-session FileReadTracker(loopal-tool-api,仅存内容哈希、内存有界):Read 记录观测内容,Edit/MultiEdit/Write 成功后更新记录;Write 落盘前若「读过且磁盘已变」则拒绝并提示重读,不 clobber 他方编辑。经 ToolContext builder(non_exhaustive + Option 字段)注入,58 个构造点零改动。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
首个驱动真实 loopal --serve 进程打进程内 mock LLM HTTP 服务器的 Rust 行为测试——经真实 Anthropic 适配器 + HTTP/SSE wire(此前只有 Desktop/Electron 覆盖此层,Rust CLI 完全没有)。harness(support.rs):serve() 起 mock、spawn loopal --serve 带 ANTHROPIC_API_KEY/BASE_URL、loopal-ipc 驱动 initialize/agent_start、reqwest 查 /__mock/requests 请求日志。场景:基础文本、完整工具循环(tool_use→真实执行→tool_result→续turn)、503 重试恢复、max_tokens 续turn、mid-stream 断连恢复、401 fatal。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
harness 增 begin_turn/interrupt/await_settled。场景:turn 阻塞在延迟 8s 的 mock 响应时发 agent/interrupt,断言真实 agent 子进程取消在途 turn(TurnCancelled/Interrupted/AwaitingInput)而非等满延迟或 Finished。闭合报告中标记的「cross-process cancellation mid-turn — NOT COVERED」gap。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
harness 参数化 Provider(Anthropic/Google/OpenAI-Responses/OpenAiCompat):同一语义场景经 4 套生产适配器各走自己的 wire 协议,证明 agent 行为 provider 无关(journal 断言协议路由;openai_compat 经 settings.json 自定义 model_prefix)。新增 thinking 流(ThinkingStream 事件回传)、一次响应多 tool_use 并行执行。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
harness 加持久模式路径(begin_persistent 无 prompt 启动 + drain 到 idle;turn_via_message 经 AGENT_MESSAGE 每轮续接;control 发 AGENT_CONTROL),与现有 ephemeral 单轮路径解耦、零影响。场景:会话连续性(第2轮携带第1轮历史,messageCount 增长);compaction 走 wire(3 轮建历史 + /compact→摘要 LLM 调用走真实 wire→Compacted 事件)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
--serve 模式下 agent 的 kernel MCP 后端永远是 Proxy(ConnectionMcpClient
包 stdio 对端),从不自己 spawn MCP 子进程;cwd 配置里的 mcp_servers 只驱动
settle 等待与 prompt 列表。harness 据此新增 Hub dispatcher:应答
hub/mcp/{snapshot,list_tools,call_tool} 并转发 notification,使 mcp_echo
在 agent/start 内即注册。新场景覆盖:配置发现 → 代理注册 → tool_use →
代理调用 → 结果回 LLM wire → 最终文本,并断言 call_tool 请求确实抵达
Hub 侧(server/tool/args 全匹配)。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
harness 的 Hub 角色扩展 hub/secret/{get,list_names}:HarnessVault 存
名称→明文、可翻转 failing 模式(respond_error→agent 侧归类 transient
Ipc),并记录 get 请求供断言。
secrets_test:模型在 Bash command 里发 <secret_ref:NAME>,管线经
hub/secret/get 解析明文注入 shell(用明文长度而非明文本身证明注入,
避免测试自身污染对话历史),结果 redact 回 wire 形式;断言事件与
mock journal 全文永不出现明文——vault 契约端到端成立。
degraded_test:failing 模式下 3 个 ref 解析失败(各 3 次瞬态重试)
触发 HubHealth 降级阈值,1s tick poller 发 HubDegraded;turn 优雅完
成,未解析 ref 被改写为 <missing-secret:NAME> 标记;vault 恢复后下
一次成功解析发 HubRecovered。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
harness 增用户席 PermissionDesk(应答 agent/permission,可 hold 让 classifier 赢竞速)与 restart/resume_persistent(同 HOME 重启进程, resume 携 session id)。权限:ask_dangerous 下 Bash 询问-允许-执行、 拒绝-零执行-模型收 denial 续接、bypass 永不询问、classifier 判定走 同一 mock wire(should_block JSON)。恢复:杀进程后新进程 SessionHistoryLoaded 回放历史,resume 后 turn 的 LLM 请求携带旧历史 (journal messageCount 增长)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
controls:ModelSwitch 改变下轮 journal.model、Rewind 使下轮携带与被 删轮相同的历史深度、Clear 归零重来、ModeSwitch/QueryMcpStatus 事件 可见。queued:turn 流式中注入第二条消息不插队、settle 后自动成轮。 loop_guard:第 4 次相同 tool 调用触发警告注入,journal 证明警告文本 到达模型。 修复 e2e 揭示的真 bug:turn_telemetry 在 turn 末用已排空的 pending_warnings 长度覆盖 warnings_injected,TurnCompleted 恒报 0; 改为 tool phase 排空时累加,telemetry 不再重算。另:测试进程按 TMPDIR 隔离,根治并发启动的 housekeeping 误清他人活跃 bash-log 目录 的 flake。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
memory:.loopal/memory 笔记在 agent start 被索引,仅命中 body 的关键 词经 memory_recall 返回全文——回归 body 全文 FTS 架构改造。hooks: pre_tool_use(条件 Bash(*))与 post_tool_use 命令钩子在真实工具两侧 落 marker 文件,turn 不受扰动。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mock-llm journal 增 systemText(四协议解析:anthropic system 块、
responses instructions、compat system 消息、google systemInstruction),
Desktop 与 CLI 行为测试自此可断言系统提示实际内容。system_prompt:
项目根 LOOPAL.md 指令与 cwd 必须出现在真实请求的 system 里。
sandbox:default_write 下 cwd 写成功;SandboxPolicySwitch("read_only")
后同类写被拒、文件不落盘、错误结果回给模型。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
新 harness:真实 loopal --hub-only(Hub+root Agent 子进程),握手行取 addr/token,TCP attach 以 ui_client 注册,hub/route 驱动 turn,事件同 构收集,LLM 指向进程内 mock。首个场景补上 serve 套件够不到的 Hub 持有侧 MCP:Hub 从 settings spawn 真 mock_mcp_server 子进程 (LocalMcpProvider 路径),agent 经 Hub 代理注册工具,tool_use 走 LLM wire→agent→Hub→MCP 子进程→回流。至此 MCP 代理侧与持有侧 两条腿都有全栈覆盖。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ui client 注册即收到会话启动事件回放(含 McpStatusReport/AwaitingInput); turn() 曾把积压里的 AwaitingInput 当作 turn 结束导致偶发空收集。改为 attach 完成即 drain 到首个 idle,turn() 遇 AwaitingInput 一律视为本轮 settle;去掉 MCP settle 的固定 sleep(积压里已见 connected)。3 连跑稳定。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
harness 重构:HubEnv 预置钩子(先建 home/cwd 供测试铺文件再 launch)、 turn() 按 agent_name 过滤——子代理事件同流广播,只有 root 的终态事件 可结束收集;second_client 注册第二个 ui_client。 subagent:Agent 工具经真 Hub spawn 真子进程,子代理在同一 mock 上跑 自己的 LLM turn,答案回流 root 的工具结果与续接(SubAgentSpawned + journal ≥3 次调用)。observer:第二 ui_client 同步收到流文本与终态。 vault:HOME/.ssh 铺 ed25519 身份、cwd/.loopal/vaults 建真 age 加密 库,<secret_ref> 经 Hub 真实解密注入 shell(长度证明),结果脱敏, 事件与 LLM wire 全程无明文——生产密钥链完整走通。cron 不入全拓扑 套件:5 字段分钟粒度需 60s 等待,由进程内 cron_resume_e2e(可控时间) 覆盖。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Write 建文件、Read 登记(编辑前置)、Edit 改写,盘上内容断言,turn 末 TurnDiffSummary 报告改动文件;Write 含 <secret_ref:> 被预检拒绝且不落 盘;overflow:行数型大输出先被 Bash 自带头尾省略,管线 overflow-to-file (100KB)由单行大字节输出触发——preview+存盘指针,全文不进上下文。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cleanup_orphans 的 live 集来自本进程 SessionStore,但共享 $TMPDIR/loopal 被机器上所有 loopal 实例写入——另一 HOME 的进行中会话在本进程眼里就是 "孤儿",删除会与其下一次 bash 日志创建竞态(e2e 套件实测踩中:exec failed: create log file ... No such file or directory)。新鲜 mtime 是唯一 跨 HOME 存活信号:目录(含两层内文件)静默满 min_age(生产 48h)才可删; 元数据不可读一律视为活跃。_in 缝隙显式传 min_age,既有行为用例传 ZERO, 新增 foreign 会话存活用例。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
auto:mock usage 报 98 万 input(98% 窗口),下一 turn 边界自动触发 summarization 走 wire(Compacted/CompactProgress + journal ≥3 调用)。 interrupt:Bash sleep 8 执行中 agent/interrupt,turn 快速取消不等睡完。 abort:第 6 次相同调用被 loop 检测拒绝执行(5 个 ToolResult+原因上报), turn 终止而非无限烧 token。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
用户席扩展 agent/question(kind=answered+labels)与 agent/plan_approval (approve/reject)应答并记录。ask_user:模型提选项问题,席位答案经工具 结果回到模型续接。plan_mode:两轮舞步——过早 ExitPlanMode 被拦截且 错误信息带出运行时随机 slug 的 plan 路径,测试提取路径代写计划文件 (受测契约是审批回路,静态场景无法预知 slug),第二轮 ExitPlanMode 触发 真实审批请求,approve 后计划全文回模型、ModeChanged 恢复 act,审批 请求载荷含计划内容。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
两个 agent 行为套件(serve 37 场景 + hub 拓扑 4 场景)按 desktop-e2e 同一约定接入门禁:tags=[e2e,manual] 使 //... 三平台矩阵(含 Windows) 不再误跑,新增 macOS 专属 Agent E2E job 显式执行;套件根加 cfg(unix) 与既有二进制 e2e 一致。共载加固:门禁 job 两套件并行,observer 收集 不再把安静间隙当结束,cli/hub 预算放宽到 40s/60s(绿路径无感,只加 失败判定余量),并发 2 连跑验证。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
metahub-settings(main 上挂 ubuntu 的那例)暴露真产品 bug:表单初始 是占位默认值,持久化设置异步到达;加载落地前点 Save/Join 会把占位 hubName(loopal-desktop)写回持久层并以错误名入会,还会覆盖用户已存 配置。修复:loaded 门禁——设置到达前禁用全部输入与写设置的动作 (Save/Clear token/Join);测试等到已加载的 hubName 显示后再交互。 workbench.child-agent(本 PR 挂 windows 的那例):agent 树随异步会话 详情填充,慢机器上晚于会话标题;同步 getByRole 改 findByRole 重试, 选择后的内容断言改 waitFor。同类反模式全套件扫描:agent-topology/ session-panel-zone 是同步 props 渲染,无竞态,不动。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
重试退避加 ±20% jitter 后,"Retrying in 2.0s"/"30.0s" 的精确文案断言
变成每条 ~12% 命中率的彩票(本轮 72 specs 仅挂 1 例纯属运气)。三个
provider spec 共 9 处:出现断言改 /Retrying in \d+\.\ds/(30s 长重试用
\d{2} 区分量级),消失断言改 not.toContainText('Retrying in')——断言
语义回归本意"横幅出现过/清除了",与 jitter 化行为一致。本地
e2e_llm_backend 全量 72 specs 验证通过。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
loopal-default-settings(本轮 CI macOS 挂的那例)与 workbench.context 同型:waitFor 等到的是 mock 被调用,随后的同步 DOM 断言竞速 promise resolve 后的状态提交。两处改为 waitFor 内容断言。全套件同型扫描: session-create-dialog/agent-control 处断言的是稳定旧状态,非竞态,不动。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Windows 上一持有者删除锁文件的 pending-delete 窗口会让并发者的 create_new 报 ACCESS_DENIED(而非 AlreadyExists),当前按致命错误直接 返回导致并发写 settings.local.json panic(CI windows 实测);改为与 AlreadyExists 同样重试。atomic 写的 rename 目标被 AV/索引器短暂持有 同理,windows 路径加有界重试,unix 路径不变。 loader_settings env 测试补 HOME/USERPROFILE 隔离——load_config 合并 ~/.loopal 全局层,开发机真实配置会泄进"默认值"场景(本机实测挂)。 child-agent 两处 findBy/waitFor 显式 15s 超时:满载 CI worker 上会话 详情提交以秒计,默认 1s 重试窗不够(vitest testTimeout=30s 余量内)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI DOM 转储证实根因不是延迟:treeitem 一直在 DOM,但 Agents tab 是 "点击已选中即折叠"的 toggle 语义,盲点击与面板初始展开状态竞速,点 在自动展开落地后就把面板折叠(aria-expanded=false + hidden),byRole 永远查不到。改为收敛式 selectAgent:每次重试先"折叠则展开"再选节 点,查询限定在本次 render 的容器内(兼防残根歧义)。3 连跑验证。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要
建立 Loopal agent 的全栈行为测试体系:真实进程 × 真实 LLM wire(进程内 mock LLM HTTP 服务器 × 生产 provider 适配器),覆盖 41 个场景,并接入 PR 门禁(新增 Agent E2E job)。测试过程咬出并修复 2 个真产品问题。
两个测试家族
tests/e2e/cli_llm/(37 场景) — 真实loopal --serve进程,stdio IPC 驱动,harness 扮演 Hub 角色(应答hub/mcp/*、hub/secret/*、agent/permission|question|plan_approval):<missing-secret:>systemText,四协议解析)tests/e2e/hub_llm/(4 场景) — 真实loopal --hub-only全拓扑(Hub + root Agent 子进程),TCP attach 以 ui_client 驱动:LocalMcpProvider路径,serve 套件原理上够不到)测试咬出的产品修复
warnings_injected恒零:turn 末 telemetry 用已排空的pending_warnings覆盖累计值 → 改为 tool phase 累加cleanup_orphans以本进程注册表判孤儿,会删掉其他 HOME 活跃会话的 bash-log 目录(与其日志创建竞态)→ 加 48h 静默期守卫架构决策记录
--serve模式 kernel 的 MCP/secret 后端永远是代理(stdio 对端即 Hub)——harness 扮演 Hub 是拓扑忠实的测法;Hub 持有侧走 hub_llm 家族tags=[e2e,manual]出三平台矩阵,新增 macOS 专属 Agent E2E job 显式执行;套件根cfg(unix);共载加固(并发 2 连跑验证)🤖 Generated with Claude Code