内容
# clint
**CLI**交互测试工具 — 驱动和测试任意交互式CLI,适用于基于目标的场景,包括AI编码助手,如 **Copilot CLI**、**Claude Code** 和 **Codex**。
不再使用具有确切按键的脆弱脚本,而是编写基于目标的YAML场景,让AI代理通过MCP驱动终端 — 类似于Playwright MCP,但适用于命令行。clint在tmux中运行CLI,观察屏幕,决定下一步操作,并生成SVG截图和HTML报告。
**人们使用clint的两种方式:**
- **测试您自己的交互式CLI** — 向导、提示、选择器 — 端到端,无需硬编码按键。
- **测试AI辅助CLI流程** — 将场景指向Copilot CLI、Claude Code或Codex,并断言助手实际完成任务(参见[`scenarios/copilot-hello-world.yaml`](scenarios/copilot-hello-world.yaml))。
## 工作原理
```
┌──────────────┐ MCP(stdio) ┌──────────────┐ tmux ┌──────────────┐
│ Copilot CLI │ ◄───────────────► │ MCP Server │ ◄───────────► │ 任意 CLI │
│ (读取YAML) │ 工具/资源 │ (clint) │ 发送按键 │ (交互式)│
└──────────────┘ └──────────────┘ 捕获窗格 └──────────────┘
```
1. **MCP服务器** 通过stdio公开终端控制工具
2. **Copilot CLI** 读取具有高级目标的场景文件
3. **tmux** 在 detached 终端会话中运行CLI命令
4. Copilot CLI **观察** 终端状态并 **决定** 要采取的操作
5. **截图** (SVG)和 **HTML报告** 自动生成
## 快速入门
### 1. 安装前提条件
<details>
<summary><strong>macOS / Linux</strong></summary>
```bash
brew install tmux # 终端后端(macOS)
# 或:sudo apt install tmux (Debian/Ubuntu)
```
</details>
<details>
<summary><strong>Windows</strong></summary>
MCP服务器直接调用`tmux`,因此在Windows上,**tmux和MCP服务器必须在WSL中运行** (Windows子系统Linux)。Windows端的`python.exe`无法找到`tmux`,并会失败,显示`ERROR: tmux is required`。
```powershell
wsl --install # 如果WSL尚未设置
wsl sudo apt update
wsl sudo apt install -y tmux
wsl tmux -V # 验证,例如 "tmux 3.6"
```
> **注意**:您将在第2步中设置一个单独的Linux虚拟环境(venv),并在第3步中注册MCP服务器通过`wsl`启动。
</details>
### 2. 克隆并设置venv
<details>
<summary><strong>macOS / Linux</strong></summary>
```bash
git clone https://github.com/banrahan/clint.git
cd clint
uv venv .venv --python 3.12
source .venv/bin/activate
uv pip install -e .
```
> **注意**:如果您没有`uv`,请使用`python3.12 -m venv .venv && source .venv/bin/activate && pip install -e .`
</details>
<details>
<summary><strong>Windows(PowerShell + WSL)</strong></summary>
由于MCP服务器必须在WSL中运行(请参见先决条件),因此在WSL中创建venv,**而不是在Windows端**。您可以将仓库克隆到Windows文件系统,并在WSL中通过`/mnt/c/...`访问它。
```powershell
git clone https://github.com/banrahan/clint.git
cd clint
# 在WSL中安装uv(一次性)
wsl bash -lc "curl -LsSf https://astral.sh/uv/install.sh | sh"
# 在仓库中创建Linux虚拟环境并安装软件包(editable)
wsl bash -lc "cd /mnt/c/Repos/clint && ~/.local/bin/uv venv .venv-wsl --python 3.12 && ~/.local/bin/uv pip install --python .venv-wsl/bin/python -e ."
# 健全性检查
wsl /mnt/c/Repos/clint/.venv-wsl/bin/python -c "import clint.mcp_server; print('OK')"
```
> **注意**:如果您不想使用`uv`,请在WSL中替换为`python3 -m venv .venv-wsl && .venv-wsl/bin/pip install -e .`。
> **注意**:调整`/mnt/c/Repos/clint`为仓库的实际克隆路径。
</details>
### 3. 将MCP服务器注册到Copilot CLI
将以下内容添加到您的Copilot CLI MCP配置中,位于 **`~/.copilot/mcp-config.json`** (如果文件不存在,请创建):
<details>
<summary><strong>macOS / Linux</strong></summary>
```json
{
"mcpServers": {
"clint": {
"type": "stdio",
"command": "<FULL-PATH-TO-REPO>/.venv/bin/python",
"args": ["-m", "clint.mcp_server"],
"cwd": "<FULL-PATH-TO-REPO>"
}
}
}
```
示例路径:`/Users/you/working/clint`
</details>
<details>
<summary><strong>Windows</strong></summary>
在Windows上,服务器通过`wsl`启动,以便访问`tmux`。将`command`指向`wsl`,并使用bash登录shell进入仓库(通过其`/mnt/c/...`路径),执行Linux虚拟环境的Python:
```json
{
"mcpServers": {
"clint": {
"type": "stdio",
"command": "wsl",
"args": [
"bash",
"-lc",
"cd /mnt/c/Repos/clint && exec .venv-wsl/bin/python -m clint.mcp_server"
]
}
}
}
```
将`/mnt/c/Repos/clint`替换为WSL中克隆的实际路径(Windows路径如`C:\Repos\foo`变为`/mnt/c/Repos/foo`)。
</details>
> **重要**:在macOS/Linux上,使用完整的**venv Python**路径,而不是系统Python。在Windows上,通过`wsl`启动,以便`tmux`在PATH上;Windows端的`python.exe`将失败,显示`ERROR: tmux is required`。
### 4. 运行场景
打开一个新的**Copilot CLI**会话(以便它获取配置),然后说:
```
使用clint加载位于scenarios/smoke-test.yaml的场景,
然后启动会话并实现目标。如果场景声明了pre或post钩子,
请在会话之前/之后运行它们。在每个步骤中拍摄截图。
```
就是这样。Copilot CLI将使用`load_scenario`读取目标,然后调用`run_pre_hooks`(如果有)、`start_session`、`observe`、`send_action`、`finish_session`和`run_post_hooks`(如果有)来驱动CLI。
## 测试AI编码助手
clint不仅用于测试**您自己的**CLI — 被测试的CLI本身也可以是AI编码助手。将场景指向 **Copilot CLI**、**Claude Code** 或 **Codex**,给出一个目标任务,让clint验证助手实际完成任务(文件创建、命令运行、预期输出存在)。
这对于:
- **冒烟测试助手的设置** (身份验证、权限、工具访问)在CI中。
- **回归测试提示或代理流程** 与实际终端输出对抗。
- **比较不同助手** 在同一任务上,使用相同的可重放场景。
`scenarios/`中提供了两个现成的示例:
- [`copilot-hello-world.yaml`](scenarios/copilot-hello-world.yaml) — 驱动Copilot CLI创建并运行`hello.py`。
- [`claude-code-hello-world.yaml`](scenarios/claude-code-hello-world.yaml) — 通过Claude Code驱动相同的任务。
> 这两个都传递非交互式权限标志(例如Copilot的`--allow-all-tools`,Claude Code的`--dangerously-skip-permissions`),因此助手在一次性工作目录中运行。仅在一次性工作目录中执行此操作。
## 编写场景
场景是YAML文件。您提供要运行的命令和描述您期望发生的事情的**目标** — Copilot CLI会找出按键。
### 结构化目标(步骤列表)
最适合需要特定序列时:
```yaml
name: "npm-init"
command: "npm init"
cwd: "~/working/my-project"
goals:
- "输入'my-app'作为包名"
- "接受默认版本"
- "输入'A sample application'作为描述"
- "等待package.json被创建"
```
### 自由文本目标
最适合探索性或灵活的流程:
```yaml
name: "interactive-setup"
command: "python setup_wizard.py"
cwd: "~/working/my-project"
goal: |
完成交互式设置向导。
选择所有提示的推荐默认值。
当询问项目名称时,输入"demo"。
等待"设置完成"消息。
```
### 场景字段
| 字段 | 必需 | 描述 |
|-------|----------|-------------|
| `name` | 是 | 场景名称(用于报告标题) |
| `command` | 是 | 在tmux中运行的CLI命令 |
| `cwd` | 否 | 工作目录(支持`~`,如果不存在则创建) |
| `env` | 否 | 额外的环境变量 |
| `goals` | 目标/目标之一 | 步骤目标描述列表 |
| `goal` | 目标/目标之一 | 单个自由文本目标描述 |
| `pre` | 否 | `start_session`前运行的主机shell钩子列表(见下文) |
| `post` | 否 | `finish_session`后运行的主机shell钩子列表(见下文) |
| `allocate_ports` | 否 | 为`{name}`替换预留空闲TCP端口(见下文) |
### 并行性和端口分配
绑定固定端口的CLI(例如`azd ai agent run`默认为`8088`)无法在不发生端口冲突的情况下并行运行 — 或与`--local`调用配对 — 除非声明`allocate_ports:`在场景中预留每个**场景运行**的空闲操作系统分配的端口,并通过`{name}`占位符在`command`、`cwd`、`env`、钩子字段和`goals`/`goal`字符串中公开它们。
两种形式:
```yaml
allocate_ports: [agent] # 命名 → {agent}
# 或
allocate_ports: 2 # 编号 → {port1},{port2};{port}别名{port1}
```
一个池**在每个`start_session`调用中跨所有`start_session`调用共享相同的`scenario_path`**,因此一个场景的两个会话看到相同的`{agent}`值 — 让`run`会话和`invoke --local`会话找到彼此。 当最后一个场景会话结束时,池自动释放。
示例:并行`azd ai agent run` + `invoke --local`:
```yaml
name: "parallel-agent"
allocate_ports: [agent]
command: "azd ai agent run --port {agent} --no-inspector"
cwd: "/tmp/parallel-agent"
goals:
- "启动代理。打开第二个会话(session_id='invoke')并使用命令:
azd ai agent invoke --local --port {agent} 'Hi'
确认两个会话都报告端口{agent}。"
```
Copilot CLI将:
1. `load_scenario(...)` — 看到`Allocated ports: agent=49733`和已经替换的目标文本中的`{agent}`。
2. `start_session(scenario_path=..., command="azd ai agent run --port {agent} --no-inspector", session_id="run")`
— 替换 + tmux启动。
3. `start_session(scenario_path=..., command="azd ai agent invoke --local --port {agent} 'Hi'", session_id="invoke")`
— 相同的`scenario_path` → 相同的池 → 相同的端口。
#### 每次调用的`session_vars`
`start_session(..., session_vars={"name": "demo"})`为调用添加额外的`{name}`替换(如果您根本没有场景文件,则很有用)。它们优先于已分配的端口,如果名称冲突。
#### 运行一个场景的N个并行实例
要运行**相同的场景**多次并发(例如压力测试,或扩展N个独立代理),请为每个调用传递不同的`instance_id`。每个`instance_id`都有自己的端口池,并且该值作为`{instance}`公开用于替换:
```yaml
name: "parallel-instances"
allocate_ports: [agent]
cwd: "/tmp/agent-{instance}"
command: "azd ai agent run --port {agent} --no-inspector"
```
```text
start_session(scenario_path=..., instance_id="1", session_id="run-1", ...)
start_session(scenario_path=..., instance_id="2", session_id="run-2", ...)
start_session(scenario_path=..., instance_id="3", session_id="run-3", ...)
```
每个调用都会实现一个单独的池(`{path}#1`、 `{path}#2`,...),分配一个独立的`{agent}`端口,并将`{instance}`解析为`"1"`、 `"2"`、 `"3"`。在`instance_id`省略时,`{instance}`默认为`"main"`,因此单个实例的场景保持不变。
### 前后钩子
`pre`和`post`声明主机shell命令(在tmux外部运行)用于设置和清理 — 例如,创建工作目录、`git init`、种子fixture文件,然后在运行后销毁它们。 Copilot CLI通过`run_pre_hooks`和`run_post_hooks`MCP工具调用它们。
每个条目要么是一个**字符串**(命令),要么是一个具有以下架构的**映射**:
| 字段 | 默认值 | 描述 |
|-------|---------|-------------|
| `run` | 必需 | shell命令(使用`bash -c`语义,因此管道/ `&&`有效) |
| `cwd` | 场景`cwd` | 工作目录;如果缺失则创建 |
| `env` | `{}` | 继承环境变量的额外环境变量 |
| `continue_on_error` | `false` | 如果`true`,非零退出 **不** 终止剩余的钩子 |
| `timeout` | `120` | 每个钩子的超时时间(秒) |
| `name` | `run`值 | 显示的结果摘要中的标签 |
执行是**顺序且快速失败**:第一个失败的钩子中止阶段,除非该钩子选择`continue_on_error: true`。中止后会将后续钩子报告为`SKIPPED`。
```yaml
name: "with-setup"
command: "my-cli init"
cwd: "/tmp/with-setup"
pre:
- "rm -rf /tmp/with-setup"
- run: "mkdir -p /tmp/with-setup"
name: "创建工作目录"
- run: "echo 'hello world' > hello.txt"
cwd: "/tmp/with-setup"
goals:
- "接受默认项目名称"
- "等待'Done'"
post:
- run: "rm -rf /tmp/with-setup"
continue_on_error: true```
> **注意(Windows / WSL)**:MCP服务器在Windows上运行在WSL上,因此`cwd`和`run`值被解释为Linux路径(例如,`/mnt/c/Repos/foo`)。场景YAML被信任输入 — 它们执行任意shell命令主机。
## MCP工具参考
这些是Copilot CLI将自动使用的工具。您不直接调用它们 — 它们在这里记录,以便您了解正在发生的事情并编写更好的场景目标。
### `load_scenario`
读取场景YAML文件并获取目标/配置。公开任何声明的前/后钩子,以便您知道调用`run_pre_hooks`/`run_post_hooks`。
| 参数 | 类型 | 描述 |
|-----------|------|-------------|
| `path` | 字符串 | YAML文件路径(绝对或相对,支持`~`) |
### `run_pre_hooks`
执行场景的`pre:`主机shell钩子列表,然后启动CLI。快速失败,除非钩子设置`continue_on_error: true`。有关架构,请参阅上面的架构。
| 参数 | 类型 | 描述 |
|-----------|------|-------------|
| `scenario_path` | 字符串 | YAML场景文件路径 |
### `run_post_hooks`
执行场景的`post:`主机shell钩子列表,然后会话结束(通常是清理)。与`run_pre_hooks`相同的语义。
| 参数 | 类型 | 描述 |
|-----------|------|-------------|
| `scenario_path` | 字符串 | YAML场景文件路径 |
### `start_session`
在tmux` | 字符串 | 所使用的场景路径 |
### `start_session`
在tmux终端中启动一个命令。`command`、`cwd`和`env`值可能包含`{name}`占位符,它们从场景的已分配端口(见下文)和`session_vars`解析。
| 参数 | 类型 | 默认值 | 描述 |
|-----------|------|---------|-------------|
| `command` | 字符串 | 必需 | CLI命令 |
| `cwd` | 字符串 | `null` | 工作目录(如果省略,则自动使用临时目录) |
| `session_id` | 字符串 | `"default"` | 会话标识符(用于并发会话) |
| `env` | 对象 | `{}` | 额外的环境变量 |
| `output_dir` | 字符串 | `"reports"` | 存储截图/报告的位置 |
| `run_name` | 字符串 | 时间戳 | 此运行的文件夹名称 |
| `scenario_path` | 字符串 | `null` | 与同一场景的其他会话共享场景的端口池 |
| `session_vars` | 对象 | `{}` | 此调用的额外`{name}`替换 |
### `release_scenario_ports`
释放一个场景的共享端口池。`finish_session`会在场景的最后一个会话结束时自动释放 — 调用此函数仅在错误路径中放弃场景而无需完成每个会话。
| 参数 | 类型 | 描述 |
|-----------|------|-------------|
| `scenario_path` | 字符串 | 传递给`start_session` / `load_scenario`的路径 |
### `observe`
读取当前的终端文本。在每次操作后调用以查看结果。
| 参数 | 类型 | 默认值 | 描述 |
|-----------|------|---------|-------------|
| `session_id` | 字符串 | `"default"` | 哪个会话被观察 |
### `send_action`
向交互式CLI发送按键。
| 参数 | 类型 | 使用 | 描述 |
|-----------|------|-----------|-------------|
| `action` | 字符串 | 全部 | 以下之一:`select`, `confirm`, `input`, `multi_select`, `wait` |
| `choice_index` | int | `select` | 要选择的项目索引(从0开始) |
| `choice_text` | 字符串 | `select` | 匹配的文本标签 — 参见“`choice_text`如何解析” |
| `value` | 布尔值 | `confirm` | `true`表示是,`false`表示否 |
| `text` | 字符串 | `input` | 要输入的文本(空 = 接受默认值) |
| `toggle_indices` | list[int] | `multi_select` | 要切换的索引列表 |
| `seconds` | 浮点数 | `wait` | 暂停时间 |
| `session_id` | 字符串 | 全部 | 操作的会话 |
#### `choice_text` 如何解析
对于调查样式的选取器(显示 `筛选: 输入以筛选列表` 的类型),
`select(choice_text="...")` 运行一个三阶段查找:
1. **是否已经高亮?** 捕获窗格;如果高亮行(带有 `>` 或 `❯` 的行)已经包含目标作为不区分大小写的子字符串,则按 Enter 并返回。
2. **筛选输入。** 否则,将目标文本键入选取器的筛选器,稍候片刻,如果高亮现在匹配,则按 Enter。
3. **滚动备用。** 如果筛选输入没有成功(较旧的不可筛选选取器,多行标签等),请用退格键清除筛选器,并使用箭头键滚动,在第一个高亮匹配项上按 Enter。
如果这三个阶段都没有找到匹配项,**`select` 引发 `LookupError` **。MCP `send_action` 工具在响应正文中显示该错误(前缀为 `ERROR during 'select': …`),以便代理看到失败而不是静默错误选取。
> **测试运行注意事项。** 不要在每次 `select` 后 `observe` 以 "验证" 选取 - 这些场景是为了 **测试** CLI 在驱动下的运行,而反应性验证掩盖了非常 bug(静默错误选取,选取器回归),这些运行本意是要捕获的。请发送操作并让下游提示显示任何失败。`LookupError` / `ERROR during 'select': …` 表面覆盖了 "目标不在列表中" 的情况 - 将其视为硬故障,而不是重试信号。
### `screenshot`
捕获终端为 SVG 文件。
| 参数 | 类型 | 默认值 | 描述 |
|-----------|------|---------|-------------|
| `label` | 字符串 | `""` | 报告中截图的标签 |
| `session_id` | 字符串 | `"default"` | 要捕获的会话 |
### `finish_session`
终止 tmux 会话并生成 HTML 报告。
| 参数 | 类型 | 默认值 | 描述 |
|-----------|------|---------|-------------|
| `session_id` | 字符串 | `"default"` | 要结束的会话 |
## 输出
每个会话都会生成一个带时间戳的目录:
```
screenshots/
agent_20240101_120000/
step_000.svg # 步骤 0 的终端截图
step_000.txt # 纯文本捕获
step_001.svg
...
final.svg # 最终终端状态
result.json # 结构化结果
report.html # 带有嵌入截图的 HTML 报告
```
在浏览器中打开 `report.html` 以查看测试运行。
## 编写良好目标的提示
- **具体说明要查找的文本**: "等待 'Next:' 出现" 比 "等待它完成" 好
- **为选项命名**: "选择 'Python' 作为语言" 比 "选取一种语言" 好
- **说明如何处理默认值**: "接受默认值" 或 "输入 'my-agent' 作为名称"
- **描述错误处理**: "如果提示现有清单,确认是"
- **在关键时刻截图**: 在提示中包含 "截取屏幕截图" 发送给 Copilot CLI
## 故障排除
| 问题 | 解决方法 |
|---------|-----|
| "tmux is required" | macOS: `brew install tmux`. Linux: `sudo apt install tmux`. Windows: 在 WSL 中安装 tmux **并** 注册 MCP 服务器通过 `wsl` 启动(参见 Windows 设置)- 从 Windows 端运行 Python 服务器将看不到 tmux。 |
| MCP 服务器未找到 | 检查 `command` 在 mcp-config.json 中的路径是否指向 venv Python |
| "No active session" | 在 `observe`/`send_action` 之前调用 `start_session` |
| 终端显示旧状态 | 调用 `observe` - 它会等待内容更改 |
| 卡在提示上 | `send_action` 等待类型可以暂停;或者尝试使用 `choice_index: 0` 的 `select` 以接受高亮的默认值 |
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器