关于 testing-tui-runtime
此技能用于在真实的 ralph-tui 终端界面中验证运行时和端到端测试结果。它提供隔离的测试环境、模拟 Agent、交互场景,以及解读会话、信号和锁行为的指引。
主要功能
- 01构建并运行已发布的 CLI
- 02创建独立临时项目和任务存储
- 03使用无需 AI 凭据的模拟 Agent
- 04测试 TUI 控件和监视行为
- 05检查信号、会话和锁文件
适用场景
- 01验证暂停、恢复和退出改动
- 02测试监视模式下的任务拾取
- 03排查信号处理和过期锁
- 04与 main 工作树构建进行行为对比
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ subsy/ ralph-tui --skill testing-tui-runtime安装后在 Claude Code 中输入/testing-tui-runtime确认已加载。
详细介绍
ralph-tui TUI 的运行时/E2E 测试
当需要在真实 TUI 中验证更改(watch 模式、引擎状态标签、暂停/恢复、关闭/退出路径),而不是通过单元测试验证时,请使用此流程。
构建并运行
cd /path/to/ralph-tui && bun run build # produces dist/cli.js
运行构建后的 CLI(不要使用 bun run dev),以确保行为与发布版本一致:
bun /path/to/ralph-tui/dist/cli.js run [flags]
临时任务跟踪器(切勿触碰仓库自己的 .beads)
创建一个带有独立 beads 存储的临时项目目录,并使用 bd 执行各个场景:
export RALPH_TEST_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ralph-tui-run.XXXXXX")" && cd "$RALPH_TEST_DIR"
bd init # or `bd create ...` which bootstraps the store
bd create "Watch mode epic" --type epic -p 1 # note the epic id bd prints
bd create "Alpha task" --type task -p 2 --parent <epic-id>
每次运行都要使用一个全新目录,不要固定使用同一个目录:复用目录会保留 .beads 任务、.ralph-tui 会话文件、锁和保护机制——而这些正是此处大多数场景要验证的内容。因此,来自上一次运行的 Recovered stale session 横幅或过期锁提示,会被误认为是当前受测构建的问题。唯一的目录还会为本次运行生成唯一的 epic id,可通过它定位对应进程(见下方的信号 trap)。
注意事项:
bd命令必须在临时目录中运行,否则会出现“no beads database found”。- 使用
--tracker beads时,TUI 必须指定--epic <id>;否则 TUI 会显示“No epics found”,而交互式 epic 选择会阻塞自动化。 - 即使 AGENTS.md 中提到了
br(beads-rust),它也可能未安装;bd可以正常使用。
模拟 Agent(无需 AI 凭据)
将插件放入 ~/.config/ralph-tui/plugins/agents/:
echo.ts— 立即返回echo-agent: local fake response\n<promise>COMPLETE</promise>。slowecho.ts— 与上面相同,但会以可中断的 500 ms 间隔休眠约 25 秒。测试迭代中途退出/暂停时必不可少;即时响应的 Agent 不会留给你中断的时间。
使用 --agent echo / --agent slowecho 进行选择。
让时序在屏幕上清晰可见
包装 CLI,以便在录制中证明退出延迟:
#!/bin/bash
cd "${RALPH_TEST_DIR:?export RALPH_TEST_DIR to the scratch dir first}"
export PATH="$PATH:$HOME/.local/bin"
echo "START $(date +%H:%M:%S.%3N) args: $*"
bun /path/to/ralph-tui/dist/cli.js run "$@"
echo "=== PROCESS EXITED code=$? at $(date +%H:%M:%S.%3N) ==="
exec bash
典型调用:
./runwatch.sh --watch --poll 60 --agent echo --tracker beads --epic <epic-id> --no-setup --force
使用较长的 --poll(60s),让暂停/退出卡住的问题一目了然;若希望快速自动拾取,则使用较短的间隔(10s)。
TUI 按键
s 启动 · p 暂停/恢复 · q → 退出对话框 → y · d 仪表板 · r 刷新。
标题栏状态标签:Waiting(watch 空闲)、Selecting、Executing、Paused、Complete。
watch 空闲时,仪表板会显示 Waiting for new tasks。
已知陷阱
- 中断(Ctrl+C)路径:在包含 #431 修复的构建中(
src/tui/utils/keyboard-shortcuts.tsRunApp.tsx中的onInterruptRequest+run.tsx中的exitSignals: ['SIGQUIT','SIGABRT']), 键盘 Ctrl+C 和外部kill -INT <pid>都会显示⚠ Interrupt Ralph?对话框,且画面保持不变;按n/Esc可取消,按y会在约 2 秒内以退出码 0 退出,并将当前任务重置为open;约 1 秒内再次按 Ctrl+C 会通过process.exit(1)强制退出(包装器code=1, 终端画面不会重绘;在exit处理器调用releaseLockSync的构建中,强制退出后 不再残留锁文件,旧版本则会残留)。 在旧版本中,Ctrl+C 完全无效,而kill -INT会清空画面并导致进程挂起,因此 在归咎于某项更改之前,务必确认当前使用的是哪个构建。
- 在大多数终端中,Ctrl+Shift+C 与 Ctrl+C 无法区分:konsole 对两者都发送字节
0x03(可用stty -isig; cat -v验证——Ctrl+Shift+C 会打印^C,Alt+C 会打印^[c),因此当 终端本身未拦截该快捷键时,“复制”快捷键会打开中断对话框(konsole 仅在存在 konsole 级别的文本选区时才会拦截)。Alt+C 则能安全地区分开。 在没有xclip/xsel/wl-paste的机器上,无法验证剪贴板内容(这就是src/utils/clipboard.ts在 Linux 上调用的命令)——请记录为未测试剪贴板写入。 - 启动早期的信号需要把握时机,且行为特殊:应用的 SIGINT/SIGTERM 处理器大约在启动后
~0.7 秒才会安装。使用以下命令驱动:
( sleep 0.5; kill -INT "$(pgrep -f "cli\.js run.*--epic $EPIC_ID")" ) & ./runwatch.sh ..., 其中$EPIC_ID是本次运行的 epic——发送信号前,检查匹配模式是否精确匹配一个 pid (pgrep -cf ...),并注意包装脚本与其启动的 CLI 是不同的进程,因此应向匹配到的 pid 发送信号,而不是包装器。不要使用pkill -f 'dist/cli.js run':它会命中机器上的所有 ralph 运行,包括并行测试或你正在比较的mainworktree 构建,并修改它们的会话和任务状态。 历史行为(src/session/lock.ts中的 #434 修复之前):在该时间窗口内收到 SIGINT 会以130退出,但留下陈旧锁;SIGTERM 则会送达但被忽略(TUI 挂载后继续运行)。在由registerLockCleanupHandlers接管启动信号的构建中(受process.listenerCount(signal) > 1保护),无论在窗口内延迟多久,SIGTERM 都应返回code=143,SIGINT 都应返回code=130,且.ralph-tui/ralph.lock应被删除;在acquireLockWithPrompt之前(约 0.05 秒)送达的信号会采用默认处置方式,但不会留下锁。 务必测试多个延迟(0.05/0.2/0.3/0.4/0.5/0.7 秒),记录信号确实已发送,并在每次测试后检查ls .ralph-tui/ralph.lock以及下次启动时显示的横幅。在该窗口内快速连续发送两个信号, 仍应只产生一次干净的 130/143,绝不能是code=1。 Recovered stale session与Stale lock detected并非一回事:锁文件可以被正确删除,但.ralph-tui/session.json中仍保留status: "running",此时每次后续启动都会打印⚠ Recovered stale session / Session status set to "interrupted" (resumable)——即使是正常执行q/y退出(code=0)之后也是如此。在将该横幅归因于锁或信号更改之前, 请检查jq .status .ralph-tui/session.json和session-meta.json;这可能是一个独立的会话持久化问题。- 在 TUI 模式下,非监视模式中所有任务完成后,进程不会自行退出;它会停留在
Complete状态,直到你退出。要证明“工作完成后进程会退出”,请使用无头模式:--no-tui(别名--headless),它会以退出码 0 退出并打印运行摘要。无头监视模式会 记录[watch] All tasks complete — polling every Ns并继续运行。 - 被终止的会话可能留下陈旧锁(旧版本;锁层接管启动信号且
exit处理器同步释放锁后已修复):下次启动时,会在 TUI 启动之前提示 "Remove the stale lock and continue? (Y/n)"。先回答y;过早发送的按键(例如s)会被该提示接收。优先正常执行q/y退出,以避免此问题。 - 在外部创建任务后,请等待至少一个完整的轮询间隔再多几秒,然后再判断自动获取任务是否失败。
- 锁保护文件(
.ralph-tui/ralph.lock.guard)和lockId:在会串行化锁变更的构建中,.ralph-tui/ralph.lock包含一个lockId随机数标识,且每次创建、清理陈旧锁、--force和释放操作都会通过使用O_EXCL创建的保护文件进行。实用的观察辅助工具:打印锁的pid/lockId、保护文件是否存在(及其内容)以及通过pgrep查到的 ralph pid,并在每种场景前后采样。正常执行期间不应观察到保护文件存在——它只应在变更期间出现。值得手动构造的情况:空保护文件或内容为not-json的保护文件(约 250ms 后应作为格式错误文件回收,运行正常启动)、指定了已退出 pid 的保护文件(应回收),以及指定了仍存活 pid 的保护文件(应保留:等待约 2 秒后获取失败,并显示Timed out waiting for the session lock (another ralph-tui process may be starting or exiting))。在此类构建中,如果保护文件在退出时一直被占用,超过同步等待预算,预期行为是留下锁供之后作为陈旧锁恢复,而不是删除它——此时留下锁应视为通过;挂起、停顿数秒或仍在运行的进程的锁消失,才是问题。确保手动构造的“仍存活 pid”确实处于运行状态(kill -0);已退出的 pid 会静默地将测试转变为“已退出 pid”情况。 run --resume可能失败,而独立的resume可以正常工作:run.tsx会先获取会话锁,再调用同样会获取锁并因锁已存在而拒绝的resumeSession——表现为Resuming previous session... / Failed to resume session,退出码为 1;而在同一中断会话上执行ralph-tui resume则能正常启动。此问题在main上也能复现,因此除非被测构建声称已修复,否则应视为既存问题;将其称为回归之前,务必与main的git worktree构建交叉验证。- 注意是否存在没有锁文件的孤立运行:曾观察到一次——TUI 完全没有显示任何内容,但引擎仍在执行迭代,且
.ralph-tui/ralph.lock不存在。这是未捕获异常处理器释放锁却未退出的特征。后续尝试未能复现;如果遇到空白 TUI,请在终止进程前,通过script -q -c "./runwatch.sh ..." /tmp/run.log包装运行,以捕获 stdout/stderr(这会保留 TTY,与管道不同)。注意runwatch.sh以exec bash结束,因此用script包装的运行会让你进入一个内层 shell——输入exit才能继续脚本循环。
Devin 所需密钥
无——虚假 Agent 插件和本地 bd 存储无需任何外部凭据。