跳转到内容

测试策略 — YAML Recipe 自测

测试策略 — YAML Recipe 自测

学习目标

  • 理解 Goose 为何选择 YAML 而非传统 Rust 测试框架来定义 Agent 行为测试
  • 掌握 goose-self-test.yaml 的结构与核心概念
  • 学会使用 goose run --recipe 执行 YAML 测试
  • 识别 YAML 行为测试的设计取舍与常见陷阱

项目实践

为什么用 YAML 做 Agent 行为测试

传统后端测试用 Rust 的 #[test]cargo test,但 Goose 的 Agent 行为测试选择了截然不同的路径——用 YAML 文件定义完整的测试场景,通过 goose run --recipe 执行。

核心原因有三点:

第一,Agent 行为本质是对话式的。 Goose 是一个与 LLM 对话驱动的 Agent,它的”行为”不是调用某个函数返回某个值,而是一系列工具调用、状态转移、错误恢复的完整流程。Rust 测试适合验证 fn parse(input) -> Result<Output, Error>,但不适合验证”先创建文件,再修改内容,然后委托子 Agent 分析,最后生成报告”这种多轮对话行为。

第二,YAML 与产品形态一致。 Goose 的 Recipe 系统本身就是 YAML 驱动的——用户编写 YAML 定义 Agent 的工作流,Goose 按 YAML 中的 instructionspromptextensions 执行。用同样的格式写测试,意味着测试本身就是 Product 的一部分,而非脱离产品的工程附属物。

第三,非工程师可编写。 YAML Recipe 的语法是自然语言指令 + 结构化参数。产品经理、QA、甚至最终用户都能读懂和编写测试用例,不需要理解 Rust 的断言宏、异步运行时、Mock 对象等概念。

测试流程概览

goose-self-test.yaml 结构解析

以项目根目录的 goose-self-test.yaml 为例,一个完整的 Agent 自测 Recipe 包含以下层次:

元数据

version: 1.0.0
title: Goose Self-Testing Integration Suite
description: A comprehensive meta-testing recipe where goose tests its own capabilities

元数据提供版本、标题和描述,用于 Recipe 的版本管理和展示。

activities — 测试阶段列表

activities:
- Initialize test workspace and logging infrastructure
- Test file operations (create, read, update, delete, undo)
- Validate shell command execution and error handling
- Analyze code structure and parsing capabilities
- Test extension discovery and management
- Test delegate tool for task delegation (sync and async)
- Test error boundaries including nested delegation prevention
- Generate comprehensive test report

activities 是高层级的测试阶段描述,类似于测试计划的目录。每个 activity 对应一个测试阶段。

parameters — 可配置参数

parameters:
- key: test_phases
input_type: string
requirement: optional
default: "all"
description: "Which test phases to run"
- key: test_depth
input_type: string
requirement: optional
default: "standard"
description: "Testing depth: quick, standard, deep"

参数化使同一个 YAML 可以执行不同范围的测试——快速冒烟、标准覆盖、深度穷尽。

instructions — 自然语言测试指令

instructions: |
You are testing yourself - a running goose instance validating its own capabilities.
## Test Execution Framework
### Phase 1: Environment Setup & Basic Tool Validation
### Phase 2: Extension System Testing
### Phase 3: Delegate & Load Testing
### Phase 4: Advanced Self-Testing
### Phase 5: Report Generation
## Success Criteria
- Phase success: 80% tests pass
- Suite success: All phases complete

instructions 用自然语言告诉 Agent 测试的整体框架、阶段划分和成功标准。这是 YAML 行为测试的核心——不是写断言,而是写”你应该怎么做测试”。

prompt — Jinja2 模板化测试用例

prompt: |
Execute the Goose Self-Testing Integration Suite in {{ workspace_dir }}.
Test phases: {{ test_phases }}, Depth: {{ test_depth }}
{% if test_phases == "all" or "basic" in test_phases %}
## PHASE 1: Basic Tool Validation
### File Operations Testing
1. Create test files with various content types
2. Test str_replace on each file type
3. Test undo functionality
{% endif %}

prompt 使用 Jinja2 模板语法,根据参数动态生成测试用例。条件分支控制哪些阶段被执行,变量插值将参数注入到测试指令中。

extensions — 声明依赖工具

extensions:
- type: builtin
name: developer
display_name: Developer
timeout: 600

声明测试所需的扩展。developer 是内置扩展,提供文件操作、Shell 命令、代码分析等核心能力。

测试执行机制

执行命令

goose run --recipe goose-self-test.yaml

该命令加载 YAML 文件,解析模板参数,启动 Agent 会话,按照 instructionsprompt 中的指令自主执行测试。

测试阶段详解

goose-self-test.yaml 定义了五个测试阶段:

Phase 1:基础工具验证。 测试文件 CRUD + undo、Shell 命令执行(包括命令链 mkdir && cd && echo、错误处理 false || echo)、代码分析(多语言结构解析、符号聚焦、调用图)。

Phase 2:扩展系统测试。 测试 Todo 扩展的持久性、动态扩展的启用/禁用、扩展间的隔离性。

Phase 3:委托与加载测试。 这是最复杂的阶段,覆盖:

  • load() 无参发现模式——列出所有可用资源
  • load(source) 加载内置 Skill——验证 Skill 注入
  • 同步委托——同一消息内多次 delegate() 串行执行
  • 异步委托——async: true 实现并行执行
  • 后台任务取消——通过 load(source, cancel: true) 终止运行中的任务
  • 嵌套委托防御——子 Agent 尝试调用 delegate() 必须被拒绝(CRITICAL 安全测试)

Phase 4:高级边界测试。 故意触发错误路径:非法路径写入、不存在的命令执行、二进制文件分析、超长文件名、Shell 注入防御。

Phase 5:报告生成。 聚合所有阶段结果,生成详细报告和控制台摘要,包括通过率、功能矩阵、问题列表。

异步委托的并行验证

YAML 测试的一个精妙设计是验证同步 vs 异步委托的执行顺序:

同步委托在同一消息内调用 3 次,各包含 sleep 2——时间戳应相差约 6 秒(串行)。异步委托同样 3 次但 async: true——时间戳应在 5 秒内(并行)。通过时间戳对比,验证调度器的行为正确性。

测试结果的自我验证

与传统测试框架的 assert_eq!(actual, expected) 不同,YAML Recipe 测试由 Agent 自身判断通过与否。Agent 在每阶段末尾将结果写入日志文件(如 phase1_basic_tools.md),最终汇总为 detailed_report.md 和终端摘要。

成功标准在 YAML 中定义:

## Success Criteria
- Phase success: 80% tests pass
- Suite success: All phases complete, critical features work
- Each test logs: setup -> execute -> validate -> result -> cleanup

Agent 按此标准自行评估,生成 PASS/FAIL 的总体结论。

问题与规避

YAML 测试漂移

问题: 随着 Agent 能力进化,YAML 中定义的测试步骤可能不再覆盖关键行为。例如新增了 vision 能力但测试 YAML 未更新,导致视觉相关功能无测试覆盖。

规避: 在 AGENTS.md 规则中明确规定:添加功能时必须更新 goose-self-test.yaml。Recipe 的 activities 列表作为测试范围的权威清单,新功能必须在此添加对应条目。

LLM 非确定性导致测试不稳定

问题: 同样的 YAML 指令,不同次运行可能因为 LLM 输出差异而产生不同结果。Agent 可能选择不同的工具调用顺序,或生成不同格式的报告。

规避:

  • 使用 test_depth 参数控制测试深度,CI 使用 quick 做冒烟测试,人工审核使用 deep
  • 关键安全测试(如嵌套委托防御)的结果判断是二元的(成功/失败),不受 LLM 风格影响
  • 时间戳对比类测试使用宽松阈值(“约 5 秒内”而非精确值)

YAML 模板语法错误

问题: Jinja2 模板中的条件分支或变量引用拼写错误,导致 YAML 加载失败或参数注入为空。

规避:

  • 所有参数必须有 default
  • {% if %} 条件使用 or 而非 in 匹配时注意边界情况(如 "basic" in "all" 为 true 是预期外的匹配)
  • 执行前用 goose recipe explain 预览渲染结果

测试环境污染

问题: 测试在共享目录中创建文件,多次运行后残留文件影响后续测试。

规避: YAML 中的 cleanup_after 参数控制清理行为,默认 true。每个阶段使用独立的子目录,测试结束后归档到 archive/ 并清除临时产物。

设计取舍

YAML Recipe vs Rust 单元测试

维度YAML RecipeRust #[test]
适用场景Agent 端到端行为验证纯函数/模块逻辑验证
可读性自然语言,非工程师可编写需要 Rust 知识
执行速度慢(LLM 调用,分钟级)快(纳秒到秒级)
确定性低(LLM 非确定性)
覆盖范围完整对话流程单点逻辑
维护成本低(自然语言修改)中(编译、类型约束)

Goose 的策略是两者互补:Rust 测试用于验证解析器、配置加载、OTLP 层等纯工程组件;YAML Recipe 用于验证 Agent 的整体行为——文件操作是否正确、委托是否串行/并行、安全防御是否生效。

行为驱动测试 for AI Agents

传统 TDD 的”红-绿-重构”循环在 Agent 场景不适用——你无法为 Agent 的行为写出精确的期望输出。行为驱动测试的思路是:

  1. 描述期望行为(YAML instructions 中的自然语言)
  2. 提供验证标准(Success Criteria)
  3. 让 Agent 自主执行并自检
  4. 人工审核最终报告

这不是单元测试的替代,而是 Agent 特有的测试范式。它验证的是”Agent 能不能按指令完成工作”,而非”某个函数返回值是否正确”。

自我测试的价值

goose-self-test.yaml 的核心理念是”Agent 测试自身”(first-person integration testing)。Agent 既是系统又是测试者,用自己的工具验证自己的功能。这种方式的独特价值:

  • 测试环境与生产环境完全一致——没有 Mock,没有测试夹具
  • 验证的是真实工具链的集成,而非隔离的单元测试
  • 测试结果本身就是 Agent 能力的证明——能完成自测的 Agent 才有能力执行用户任务

参考来源

  • 源码:.op/goose/goose-self-test.yaml — 完整的自测 Recipe
  • 源码:.op/goose/crates/goose/src/recipe/ — Recipe 解析与构建核心
  • 源码:.op/goose/crates/goose-cli/src/recipes/ — CLI Recipe 加载与参数注入
  • 源码:.op/goose/recipe-scanner/base_recipe.yaml — Recipe 基础模板
  • 文档:.op/goose/documentation/docs/guides/recipes/recipe-reference.md — Recipe 参考文档