首页
/ Cline CLI TUI 无头测试实践:用 tuistory 驱动终端会话并编写 e2e 测试

Cline CLI TUI 无头测试实践:用 tuistory 驱动终端会话并编写 e2e 测试

2026-09-04 16:53:34作者:宣海椒Queenly

在 Cline CLI(apps/cli)仓库中,交互式的 TUI 界面长期难以在无头(headless)环境下自动测试:传统的做法是用 Unix script 工具加定时的 printf 按键、再靠 sleep 和原始输出流的 grep 来断言,脆弱且无法感知界面遮挡关系。本文基于仓库内的技能文档 .cline/skills/tuistory/SKILL.md,完整讲解如何使用 tuistory 以命名后台 PTY 会话驱动 Cline TUI——包括隔离环境启动、observe → act → observe 交互循环、后台进程管理,以及用其库 API 编写可运行的 e2e 测试(bun run test:e2e:tuistory),帮助你在云端 Agent 或 CI 环境中稳定地复现 TUI 缺陷、采集界面证据。

tuistory 是什么:Ghostty 终端模拟器之上的命名 PTY 会话

tuistory 可以把任意终端命令包装进一个命名的后台 PTY 会话,其底层由 Ghostty 终端模拟器提供终端仿真能力。它的设计定位可以类比为“终端版的 Playwright/tmux”:

  • Agent 侧:通过一系列短小的 CLI 调用(wait / type / press / snapshot / screenshot / read)与交互,每次调用立即返回,不会阻塞工具调用;
  • 人类侧:可以随时 tuistory attach -s <name> 附加到同一个会话进行观察或干预;
  • 无头友好:不需要真实终端,也不需要图形显示(无 DISPLAY 也能工作),这使它成为云端 Agent 操作 Cline TUI 的首选方式。

在 Cline 仓库中,tuistory 以 devDependency 的形式锁定在 @cline/cli 包中,版本为 ^0.10.1,见 apps/cli/package.json。因此从 apps/cli 目录内执行命令时,pinned 的二进制可以直接解析:

cd apps/cli
bunx tuistory --help   # 命令、选项与语法的权威来源

仓库中还有一个传统对照组:src/cli.interactive.e2e.test.tsscript 工具把按键按固定 sleep 时间表管道进去,然后对原始输出做 ANSI 剥离和 grep(其 normalizeTerminalOutput 需要手工剥 CSI/OSC 转义、CR、退格等噪声字节);而 src/cli.tuistory.e2e.test.ts 的文件头注释明确说明了迁移动机——每个测试把 CLI 放进 Ghostty 仿真 PTY,反应式地等待屏幕内容出现waitForText 在文本一渲染就立即 resolve),并且断言的是仿真后的屏幕状态而非原始字节流。这正是后文“屏幕状态断言”能力的来源。

隔离环境启动 Cline TUI

驱动 TUI 的第一步是把它启动在一个隔离环境里,避免污染真实用户配置(~/.cline)。参考技能文档给出的标准命令:

cd apps/cli
DATA_DIR=$(mktemp -d) && HOME_DIR=$(mktemp -d)
bunx tuistory -s cline --cols 120 --rows 36 \
  --env HOME=$HOME_DIR --env CLINE_DATA_DIR=$DATA_DIR \
  --env CLINE_DISABLE_CLINE_PASS_NOTICE=1 --env CLINE_TELEMETRY_DISABLED=1 \
  -- bun src/index.ts --provider anthropic -m claude-sonnet-4-6 -k test-key

各环境变量的作用,结合参考测试 apps/cli/src/cli.tuistory.e2e.test.ts 中的 createCliEnv() 可以进一步补全:

环境变量 作用
HOMECLINE_DATA_DIR 指向临时目录,隔离用户主目录与数据目录;测试中还派生出 CLINE_DB_DATA_DIRCLINE_SESSION_DATA_DIRCLINE_TEAM_DATA_DIRCLINE_PROVIDER_SETTINGS_PATHCLINE_HOOKS_LOG_PATH 等,全部落在同一个临时树内
CLINE_DISABLE_CLINE_PASS_NOTICE=1 关闭 ClinePass 推广弹窗。源码注释特别指出:基于流 grep 的旧测试套件感知不到这个覆盖在聊天视图上的弹窗,而 tuistory 的屏幕快照反映的是“用户实际看到的内容”,所以这个开关在无头验证中必不可少
CLINE_TELEMETRY_DISABLED=1CLINE_NO_AUTO_UPDATE=1 关闭遥测与自动更新,避免后台噪声
清除 CI / VITEST 父进程 vitest 会设置 CI/VITEST,若不显式清除,子进程 CLI 会被识别为非交互终端,无法按真实交互模式渲染

关于凭据:命令中的假密钥 -k test-key 足以让完整的聊天 UI 渲染出来,只有真正发起 Agent 回合时才会失败。如果需要回放录制的 LLM 回合,仓库提供 VCR 机制——在 apps/cli/src/index.ts 入口处即调用 initVcr(process.env.CLINE_VCR),配合环境变量 CLINE_VCR=playback + CLINE_VCR_CASSETTE=<cassette路径> 即可(该机制的完整约定见 apps/cli/src/tests/helpers/env.ts,其中 clineEnv() 会在检测到 cassette 时默认注入 CLINE_VCR: "playback"CLINE_VCR_FILTER: "")。真实回合则必须提供提供方凭据(如 ANTHROPIC_API_KEYCLINE_API_KEY)。

observe → act → observe 交互循环

会话启动后,标准操作模式是“观察 → 行动 → 再观察”,技能文档给出的完整循环如下:

# 反应式等待聊天视图出现 —— 永远不要用 sleep
bunx tuistory -s cline wait "What can I do for you?" --timeout 30000

# 行动后,始终观察结果屏幕状态
bunx tuistory -s cline type "/settings"
bunx tuistory -s cline snapshot --trim
bunx tuistory -s cline press enter
bunx tuistory -s cline snapshot --trim

# 当前屏幕的带样式 PNG 截图(输出文件路径)—— 适合作为证据产物
bunx tuistory -s cline screenshot

# 完整原始输出流(snapshot 只显示可见屏幕)
bunx tuistory read -s cline --all

# 拆除自己启动的会话(双 Ctrl+C 可干净退出 TUI)
bunx tuistory -s cline press ctrl c
bunx tuistory -s cline press ctrl c
bunx tuistory -s cline close

这里每个动作都有对应的源码级证据。参考测试 apps/cli/src/cli.tuistory.e2e.test.tsafterEach 里正是用“双 Ctrl+C + waitIdle + close()”三步完成会话拆除,注释解释了原因:第一次 Ctrl+C 只显示“再按一次退出”的提示,第二次才真正退出,随后 PTY 才被销毁(L105-L121)。snapshotread 的分工也体现在文档规则中:snapshot 反映用户实际所见(被遮挡的文本不计入),read 返回自上次读取以来的原始流。

把 tuistory 当后台进程管理器(替代 tmux)

不局限于 TUI,任何长驻/交互式进程都可以放进命名会话而不会挂起你的工具调用:

bunx tuistory -s my-server -- bun run dev:sidecar   # 立即返回
bunx tuistory -s my-server wait "/listening|ready/i" --timeout 30000
bunx tuistory read -s my-server                     # 读取自上次以来的新输出
bunx tuistory -s my-server restart                  # 改完代码后重启

要点是:启动命令立即返回;wait 支持正则(/pattern/i 为忽略大小写,默认区分大小写)并以 --timeout 兜底;restart 在迭代开发时免去手动关闭再启动。

关键规则(Key Rules)

技能文档总结的操作纪律,逐条都有实现层面的依据:

  • 选项在 -- 之前,命令在之后。 第一个 -- 之后的所有内容原样传给子进程:tuistory -s name --cols 150 -- bun src/index.ts 是正确写法。
  • 每次行动后都 snapshot。 TUI 是有状态的,对话框和错误信息会覆盖在你预期的视图之上;snapshot 反映的是用户真实所见(被遮挡文本不算),这与 grep 原始流有本质区别。
  • wait 而不是 sleep。 wait "text" / wait "/regex/i"(默认区分大小写)的反应速度与终端刷新同步;不知道预期什么时用 wait-idle。始终传 --timeout
  • 按键会立刻送达。 与 sleep 型脚本不同,排队的第二次按键可能漏进下一个视图——例如一个 Enter 会同时“接受斜杠补全 + 提交”。参考测试中的 /settings 用例特别注释了这一点:旧 script 套件按两次 Enter 并各自 sleep 250ms,而反应式按键投递下第二次 Enter 会漏进 settings 视图并激活聚焦行(L171-L181)。
  • 不要关闭不是你启动的会话。 会话是与人(tuistory attach -s name)和其他 Agent 共享的;默认让会话继续运行,用 read / wait / snapshot 做无损检查。
  • --cols/--rows 影响 TUI 布局(断言对宽度敏感,参考测试统一用 120×36);--pixel-ratio 2 可得到更清晰的截图。

用库 API 编写 e2e 测试

程序化 API 在进程内运行、无需守护进程,参考实现是 apps/cli/src/cli.tuistory.e2e.test.ts(运行命令 bun run test:e2e:tuistory,脚本定义于 apps/cli/package.jsontest:e2e:tuistory,指向 vitest.tuistory.e2e.config.ts:匹配 src/**/*.tuistory.e2e.test.ts,测试与 hook 超时均为 60s)。

启动会话的核心代码:

import { launchTerminal } from "tuistory";

const session = await launchTerminal({
	command: "bun",
	args: ["src/index.ts", "--provider", "anthropic", "-k", "test-key"],
	cwd: cliRoot,
	env: isolatedEnv, // 见参考测试中的 createCliEnv()
	cols: 120,
	rows: 36,
	waitForDataTimeout: 30_000, // CLI 冷启动需要编译大型 TS 图
});

await session.waitForText("What can I do for you?", { timeout: 30_000 });
const screen = await session.text({ trimEnd: true }); // 仿真后的屏幕状态
await session.type("/settings");
await session.press("enter");
session.close(); // 始终在测试 teardown 中 close

几个从参考测试中可以读到的关键细节:

  1. waitForDataTimeout 必须放大。 CLI 冷启动时要编译大型 TypeScript 图,默认 5s 的首数据超时不够,参考测试把启动与 UI 超时分别定为 LAUNCH_TIMEOUT_MS = 30_000UI_TIMEOUT_MS = 15_000L25-L26)。
  2. 屏幕状态断言能验证“陈旧 UI 已消失”。 例如 Tab 切换 Plan/Act 模式的用例:先 waitForText("● Plan ○ Act (Tab)"),再同时断言新指示器存在、旧指示器不存在expect(screen).not.toContain("○ Plan ● Act (Tab)")L133-L149)。这是基于流 grep 的框架做不到的——旧状态仍埋在滚动回卷缓冲里。
  3. session.text() 支持谓词等待与样式过滤。 参考测试用 session.text({ waitFor: (text) => !text.includes("Compaction") }) 等待 settings 面板从 General 页签切到 MCP 页签(L189-L193);技能文档还指出 session.text({ only: { bold: true } }) 可按样式过滤,session.read() 返回自上次读取以来的原始流。
  4. teardown 纪律。 参考测试用模块级 sessions 数组登记所有会话,afterEach 中统一“双 Ctrl+C → waitIdle(3s)close()”,并容忍会话已死的情况,同时递归清理所有临时目录(L105-L121)。
  5. 断言可穿透到磁盘副作用。 ClinePass 推广弹窗用例在按键关闭弹窗后,进一步用 waitIdle 等待落盘并断言 cli-notices.json 中写入了 "cline-cli-cline-pass-intro": true 标记(L235-L243),说明 tuistory 测试不仅能验证界面,也能验证界面背后的持久化行为。

小结与延伸阅读

tuistory 给 Cline CLI 带来的核心能力是:用反应式等待替代 sleep、用仿真屏幕状态替代原始流 grep、用命名 PTY 会话替代 tmux 手工管理,从而让无头环境(云端 Agent、CI)里的 TUI 测试变得确定且可断言。若需要进一步深入,可以按以下路径阅读仓库源码:

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
903
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341