Freebuff CLI 端到端测试实战:基于 tmux 与 Codebuff SDK 的双层测试体系
【免费下载链接】freebuffThe free coding agent项目地址: https://gitcode.com/GitHub_Trending/cod/freebuff
本文档是 Freebuff 开源仓库中 CLI 端到端(E2E)测试体系的完整技术指南。它围绕 freebuff/e2e/README.md 展开,详细讲解如何通过 tmux 驱动编译产物进行确定性断言,如何使用 Codebuff SDK 让 AI 测试代理自动验证复杂行为,以及如何对生产后端执行"真实一轮对话"的冒烟测试。读完本文,你将掌握 Freebuff E2E 测试的完整架构、核心工具类用法、构建运行流程,并能够按既有规范新增一条端到端测试。
为什么 CLI 需要端到端测试
Freebuff 是一个终端交互的编码代理 CLI(The free coding agent),其核心体验全部发生在 TUI(终端用户界面)之中:启动时的 ASCII 启动画面、模型选择器、/help等斜杠命令、流式对话输出。单元测试可以覆盖内部逻辑,却无法回答"编译出的二进制在真实终端里能不能正常启动、按键是否被正确捕获、输出是否如期渲染"这类问题。
freebuff/e2e/README.md 给出的答案是:用 tmux 模拟真实终端会话,让测试代码像真实用户一样与编译后的二进制交互——发送文本、发送按键、抓取屏幕输出、断言行为——从而验证 CLI 的端到端正确性。整个 E2E 测试目录结构如下:
freebuff/e2e/ ├── README.md ├── agent/ │ └── freebuff-tester.ts # SDK 测试代理定义 ├── tests/ # 各特性 E2E 测试 │ ├── ads-behavior.e2e.test.ts │ ├── agent-startup.e2e.test.ts │ ├── code-edit.e2e.test.ts │ ├── help-command.e2e.test.ts │ ├── knowledge-file.e2e.test.ts │ ├── live-turn.e2e.test.ts # 唯一的线上冒烟测试 │ ├── slash-commands.e2e.test.ts │ ├── startup.e2e.test.ts │ ├── terminal-command.e2e.test.ts │ └── version.e2e.test.ts └── utils/ ├── binary-helpers.ts # 二进制路径解析 ├── freebuff-session.ts # 核心会话封装类 ├── index.ts # 统一导出 ├── tmux-custom-tools.ts # SDK 自定义工具 └── tmux-helpers.ts # tmux 脚本底层封装测试架构:三种互补的验证方式
根据 README 的定义,这套体系同时支持三种测试方式,覆盖从"快速确定性回归"到"真实后端链路"的完整梯度:
| 方式 | 核心机制 | 适用场景 |
|---|---|---|
| 1. 直接 tmux 测试 | 用FreebuffSession类在 tmux 中启动二进制、发送命令、抓取输出并直接断言 | 快速、确定性的功能回归,如启动、版本、帮助命令 |
| 2. SDK Agent 驱动测试 | 通过 Codebuff SDK 运行一个测试代理,代理借助自定义 tmux 工具与 CLI 交互 | AI 推理式验证复杂行为,输出不再是"匹配字符串"而是"理解语义" |
| 3. 线上冒烟测试 | 使用生产环境变量构建二进制,真实调用后端完成一轮对话并结束会话 | 发布前与定时任务中对线上链路的冒烟验证 |
方式一:直接 tmux 测试(快速、确定性)
这是最常用的方式。测试代码通过FreebuffSession在 tmux 中启动二进制,随后发送命令、捕获输出、断言结果:
import { describe, test, expect, afterEach } from 'bun:test' import { FreebuffSession, requireFreebuffBinary } from '../utils' describe('My Feature', () => { let session: FreebuffSession | null = null afterEach(async () => { if (session) await session.stop() session = null }) test('works correctly', async () => { const binary = requireFreebuffBinary() session = await FreebuffSession.start(binary) await session.send('/help') const output = await session.capture(2) expect(output).toContain('Shortcuts') }, 60_000) })注意测试运行在bun:test之上(与仓库其余测试一致),每个测试都声明了超时(如60_000毫秒),因为真实的终端交互与轮询需要时间。
方式二:SDK Agent 驱动测试(AI 推理式验证)
当行为过于复杂、无法用简单的字符串包含断言时,可以使用 Codebuff SDK 运行一个测试代理。代理通过自定义 tmux 工具与 Freebuff 交互,理解CLI 输出并验证复杂行为:
import { describe, test, expect, afterEach } from 'bun:test' import { CodebuffClient } from '@codebuff/sdk' import { freebuffTesterAgent } from '../agent/freebuff-tester' import { createFreebuffTmuxTools, requireFreebuffBinary } from '../utils' describe('Agent Test', () => { let cleanup: (() => Promise<void>) | null = null afterEach(async () => { if (cleanup) await cleanup() cleanup = null }) test('verifies startup', async () => { const apiKey = process.env.CODEBUFF_API_KEY if (!apiKey) return // Skip if no API key const binary = requireFreebuffBinary() const tmuxTools = createFreebuffTmuxTools(binary) cleanup = tmuxTools.cleanup const client = new CodebuffClient({ apiKey }) const result = await client.run({ agent: freebuffTesterAgent.id, prompt: 'Start Freebuff and verify the branding is correct.', agentDefinitions: [freebuffTesterAgent], customToolDefinitions: tmuxTools.tools, handleEvent: () => {}, }) expect(result.output.type).not.toBe('error') }, 180_000) })无CODEBUFF_API_KEY时该测试直接跳过,因此本地未配置密钥也不会阻塞测试套件。
方式三:线上冒烟测试(真实后端链路)
freebuff/e2e/tests/live-turn.e2e.test.ts 是freebuff/e2e/tests/目录中唯一直接与真实后端通信的文件:它在落地页选择器上定位到 DeepSeek V4.1 Flash 模型、启动会话、发送一条提示词、等待回答,最后执行/end-session并校验后端不再存在打开的会话。它只有在设置了FREEBUFF_SMOKE_API_KEY(或CODEBUFF_API_KEY)时才运行,否则跳过;且需要一个以生产公共环境变量构建的二进制。该测试不在freebuff-e2e.yml矩阵中,而是由.github/workflows/prod-smoke.yml按计划(schedule)以及每次发布前执行(说明:当前仓库快照的 .github/workflows 目录下仅可见ci.yml与pr-hygiene.yml,README 中描述的freebuff-e2e.yml、prod-smoke.yml属于文档记载的 CI 配置)。
NEXT_PUBLIC_CB_ENVIRONMENT=prod NEXT_PUBLIC_CODEBUFF_APP_URL=https://www.codebuff.com \ NEXT_PUBLIC_FREEBUFF_APP_URL=https://freebuff.com NEXT_PUBLIC_SUPPORT_EMAIL=support@codebuff.com \ NEXT_PUBLIC_POSTHOG_API_KEY=test NEXT_PUBLIC_POSTHOG_HOST_URL=http://127.0.0.1:9 \ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=test NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL=http://127.0.0.1:9 \ NEXT_PUBLIC_WEB_PORT=3000 bun freebuff/cli/build.ts 0.0.0-smoke CODEBUFF_API_KEY=<your token> bun test freebuff/e2e/tests/live-turn.e2e.test.ts --timeout=300000两个关键设计细节:
- 配置隔离:CLI 运行在自己的
FREEBUFF_CONFIG_DIR下,因此不会触碰本机的真实用户配置。从源码看(freebuff/e2e/tests/live-turn.e2e.test.ts),测试用fs.mkdtempSync创建临时配置目录,写入settings.json(设置freebucksIntroSeenAt以跳过首次运行卡片),并通过FreebuffSession.start的env选项注入CODEBUFF_API_KEY与FREEBUFF_CONFIG_DIR。 - 不留尾巴:
afterEach中若仍处于聊天界面且会话未结束,会自动发送/end-session,防止一次失败的断言在账号上留下长达一小时的开放会话(源码注释明确记录了首个 CI 运行曾因此踩坑,见 live-turn.e2e.test.ts)。
测试的提示词也刻意选择了不可靠记忆、但答案唯一的内容:"Reply with only the English word for the number 7, in lowercase.",期望输出seven(源码注释解释了为何不用算术题——Flash 曾把 4187+2359 答成 6536)。
前置条件
运行 E2E 测试前需要准备:
- tmux必须安装:macOS 用
brew install tmux,Ubuntu 用sudo apt-get install tmux; - Freebuff 二进制必须先构建:
bun freebuff/cli/build.ts 0.0.0-dev; - SDK 构建产物(仅 Agent 测试需要):
cd sdk && bun run build; - CODEBUFF_API_KEY(仅 Agent 测试需要):设置该环境变量。
构建与运行测试
先构建二进制
bun freebuff/cli/build.ts 0.0.0-dev二进制路径的解析规则见 freebuff/e2e/utils/binary-helpers.ts:优先读取环境变量FREEBUFF_BINARY,否则回退到cli/bin/freebuff;requireFreebuffBinary()在文件不存在时会抛出带构建提示的错误信息("Build with: bun freebuff/cli/build.ts ")。
运行全部测试
bun test freebuff/e2e/tests/运行单个测试
bun test freebuff/e2e/tests/version.e2e.test.ts bun test freebuff/e2e/tests/startup.e2e.test.ts bun test freebuff/e2e/tests/help-command.e2e.test.ts bun test freebuff/e2e/tests/agent-startup.e2e.test.ts使用自定义二进制路径
FREEBUFF_BINARY=/path/to/freebuff bun test freebuff/e2e/tests/核心工具:FreebuffSession源码级详解
FreebuffSession定义于 freebuff/e2e/utils/freebuff-session.ts,是整个体系的基石。它封装了"在 tmux 中启动二进制 → 与 TUI 交互 → 抓取输出 → 清理"的全过程。
API 一览
| 方法 | 说明 |
|---|---|
FreebuffSession.start(binaryPath) | 在 tmux 中启动二进制,返回会话对象 |
session.send(text) | 发送文本输入(默认按下回车) |
session.sendKey(key) | 发送特殊按键(如'C-c'、'Escape') |
session.capture(waitSec?) | 抓取终端输出 |
session.captureLabeled(label, waitSec?) | 抓取并保存到会话日志(带标签) |
session.waitForText(pattern, timeoutMs?) | 轮询直到终端出现指定文本 |
session.stop() | 停止会话并清理资源 |
start():临时工作目录与安全的密钥注入
FreebuffSession.start的源码实现(freebuff-session.ts)值得仔细阅读:
- 创建临时项目目录:
fs.mkdtempSync在系统临时目录生成freebuff-e2e-*目录,并写入一个最小README.md(# E2E Test Project),让 Freebuff 有真实项目可索引; - 支持预置初始文件:通过
initialFiles选项可在启动前写入任意相对路径的文件(含嵌套目录自动创建); - 环境变量的安全注入:
env选项(如密钥、配置目录)被写入一个位于项目目录之外、权限为0600的env.sh文件,再由 tmux shellsource加载(源码见 freebuff-session.ts)。这样密钥既不会落入 CLI 索引的项目目录,也不会出现在会话记录的命令行中; - tmux 启动:组装
cd '<tmpDir>' && '<binaryPath>'命令并调用tmuxStart,默认等待 4 秒、窗口尺寸 120×30。
会话内文件操作与轮询
除终端交互外,FreebuffSession还提供writeFile/readFile/fileExists直接在临时工作目录中读写文件,以及waitForFileContent(relativePath, pattern, timeoutMs?)轮询等待某个文件出现指定内容(默认超时 60 秒,超时后抛出含终端输出的详细错误)。这对于验证"CLI 是否在项目中实际写出了文件"的场景(如代码编辑测试)非常实用。
启动就绪判断:waitForReady与waitForBootSignal
TUI 的启动存在多种形态,源码为此提供了两套判断:
waitForReady(timeoutMs?, minLines?):轮询终端输出,直到非空行数达到阈值(默认 5 行),表示 TUI 已完成初始渲染;waitForBootSignal(timeoutMs?):轮询直到输出命中FREEBUFF_BOOT_SIGNALS中的任意一个已知启动标志。该常量数组(freebuff-session.ts)包含:
export const FREEBUFF_BOOT_SIGNALS = [ '█████╗ ██████╔╝', // ASCII logo (full or small variant) 'Start coding for free', 'Enter a coding task', 'Pick a model to start', "Free mode isn't available", 'Press ENTER to login', 'Open this URL', 'will run commands on your behalf', ] as const源码注释解释了原因:CI 运行环境经常落在模型选择器的 wordmark("Start coding for free")而非完整的 ASCII 大 Logo,所以相比只等待单个 Logo 行,匹配一组标志更可靠。
stop():幂等清理
stop()先调用tmuxStop(该脚本幂等,会话已不存在时静默忽略错误),再递归删除临时工作目录与环境目录。
tmux 底层封装与脚本层
FreebuffSession并不直接操作 tmux,而是通过 freebuff/e2e/utils/tmux-helpers.ts 调用仓库根目录scripts/tmux/下的 shell 脚本(tmux-start.sh、tmux-send.sh、tmux-capture.sh、tmux-stop.sh):
| 函数 | 对应脚本 | 说明 |
|---|---|---|
tmuxStart(options) | tmux-start.sh | 启动 tmux 会话,支持--command、--name、--width、--height、--wait、--plain |
tmuxSend(session, text, opts?) | tmux-send.sh | 发送文本,支持--no-enter(不回车)、--wait-idle(等待空闲)、--force |
tmuxSendKey(session, key) | tmux-send.sh --key | 发送特殊按键 |
tmuxCapture(session, opts?) | tmux-capture.sh | 抓取输出,支持--wait、--label(带标签保存日志)、--no-save |
tmuxStop(session) | tmux-stop.sh | 停止会话,错误被捕获(幂等) |
这些脚本同时被 scripts/tmux 目录下的其他工具复用,构成一套独立的终端会话操作基础设施。
SDK 自定义工具与测试代理
createFreebuffTmuxTools(binaryPath)
定义于 freebuff/e2e/utils/tmux-custom-tools.ts,返回{ tools, cleanup }:tools是四个自定义工具定义(输入模式使用zod/v4schema 声明),cleanup应在afterEach中调用以兜底停止会话:
| 工具名 | 作用 | 关键行为 |
|---|---|---|
start_freebuff | 启动 CLI | 先检查会话是否已存在;启动后调用waitForReady()并返回初始输出 |
send_to_freebuff | 发送文本输入 | 如同用户输入,默认按下回车;无会话时返回错误信息 |
capture_freebuff_output | 抓取终端输出 | 可选waitSeconds参数,等待后再抓取 |
stop_freebuff | 停止并清理 | 幂等,返回wasRunning状态 |
freebuffTesterAgent:专职 QA 测试代理
freebuff/e2e/agent/freebuff-tester.ts 定义了测试代理:id为freebuff-tester,模型为anthropic/claude-sonnet-4.5,可用的工具恰好是上述四个 tmux 工具。其系统提示词(instructionsPrompt)为代理规定了标准的操作流程:
- 调用
start_freebuff启动 CLI; - 用
capture_freebuff_output(带waitSeconds)查看终端输出; - 用
send_to_freebuff输入命令或文本; - 再次抓取输出验证行为;
- 完成后务必调用
stop_freebuff。
同时要求代理重点核查:CLI 是否无错误/无崩溃启动、启动画面是否有可见内容、命令是否符合预期、错误信息是否对用户友好,并清晰汇报测试项、观察结果与通过/失败结论。这套"自定义工具 + 专用代理 + 结构化指令"的模式,正是 Freebuff 用 AI 验证 AI 产品的落地示范。
测试用例剖析
version.e2e.test.ts:版本输出的三个断言
freebuff/e2e/tests/version.e2e.test.ts 直接以execFileSync运行二进制(不经 tmux),验证三点:
--version输出应匹配/\d+\.\d+\.\d+/(semver 形式);- 退出码为 0(
execFileSync遇非零退出码会抛异常,不抛即通过); - 忽略项目
bunfig.toml的 preload:测试在临时目录写入preload = ["$config/db"]的bunfig.toml后仍能正常输出版本——这验证了--version这类无界面命令不会被项目配置干扰。
startup.e2e.test.ts:启动画面与优雅退出
freebuff/e2e/tests/startup.e2e.test.ts 包含两个典型场景:
- 启动画面渲染:
waitForBootSignal()等待任一启动标志出现,随后反向断言排除致命错误标记——输出不得包含Fatal error during startup、Internal error: tree-sitter.wasm not found、FATAL、panic、Segmentation fault。这是一种"双重保险"策略:即便某个竞态导致错误与 Logo 同时出现,也会被清晰指出而非埋没在原始输出中; - Ctrl+C 优雅退出:启动并
waitForReady()后发送C-c,等待片刻再断言输出不含Unhandled与FATAL——验证 CLI 能干净地处理中断信号。
help-command.e2e.test.ts:品牌与用法校验
freebuff/e2e/tests/help-command.e2e.test.ts 检查--help输出:必须包含freebuff(忽略大小写)、匹配/usage|options|commands/i,并且不得出现codebuff字样——后者是品牌正确性的回归测试,防止帮助文案中泄露母公司品牌。
新增测试的规范流程
按 README 的指引,新增一条 E2E 测试只需三步:
- 在
freebuff/e2e/tests/下新建文件,命名遵循<feature>.e2e.test.ts约定; - 将测试名加入
.github/workflows/freebuff-e2e.yml的矩阵(README 记载的 CI 配置,示例片段):
matrix: test: - version - startup - help-command - agent-startup - your-new-test # <-- add here- 提交后,该测试将在 CI 中与其他测试并行运行。
CI 工作流设计
根据 README 对.github/workflows/freebuff-e2e.yml的描述,CI 流程为:
- 构建一次Freebuff 二进制(linux-x64);
- 通过 GitHub Actions 的 matrix 策略并行运行每个测试文件;
- 失败时上传 tmux 会话日志用于调试(这正是
captureLabeled带标签保存日志的意义所在)。
触发时机包括每晚 PT 时间 6:00与手动触发(workflow_dispatch)。线上冒烟测试live-turn.e2e.test.ts则独立于矩阵之外,由prod-smoke.yml在计划任务与每次发布前执行。
最佳实践小结
- 分层选择测试方式:确定性行为用直接 tmux 断言(快、稳、无需密钥);需要语义理解的行为交给 SDK 测试代理;线上链路用带密钥的冒烟测试且务必结束会话;
- 善用轮询而非固定 sleep:
waitForText、waitForBootSignal、waitForFileContent都采用轮询 + 超时错误报告,失败时附上终端输出便于定位; - 注意密钥与配置隔离:利用
env选项 + 0600env.sh+ 独立FREEBUFF_CONFIG_DIR,测试永不污染真实用户环境; - 每个测试声明合理超时:终端交互类测试通常以 60 秒为基准,Agent 测试与线上冒烟可达 180~300 秒;
- 反向断言防漏网:对启动等关键路径,除了正向匹配启动标志,还应反向断言
FATAL、panic等致命标记。
至此,你可以基于这套框架为 Freebuff 的任意 CLI 功能编写稳定、可并行、可调试的端到端测试,并顺畅地融入其 CI 流水线。
【免费下载链接】freebuffThe free coding agent项目地址: https://gitcode.com/GitHub_Trending/cod/freebuff
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考