# tester 运行 只在用户要跑测试时读本文件。 Agent、执行目标和临时 terminal 的完整协议见 [execution.md](execution.md)。每轮运行 都要先解析并更新 `agent`,输出生效配置列表,准备目标项目并创建一个全新的 terminal;不能复用 已有 terminal。 测试结束、失败、超时或中断时,都要关闭本轮创建的 terminal。 ## 0. 建立测试矩阵并完成全量 preflight 测试不是“找到一个能跑的设备就开始”。每轮先建立一份测试矩阵;移动端每个目标产物 必须单独占一行: ```text 平台 | 产物路径/下载地址 | commit | 包名或 Bundle ID | tester 绑定 | 执行环境 Android | … | … | page.writes | tester.android | local-app iOS | … | … | page.writes | tester.ios | remote-ios ``` 矩阵里的产物位置必须由调用方给出,值为本地文件或 URL。不能根据“曾经构建成功过”、 `builder.` 绑定、CI release 名或本机旧文件猜测本轮产物。缺少路径或下载 地址时,先报告“待确认”并停止。 平台绑定是测试执行目标,不是构建目标:`builder.ios` 只能证明构建配置,不能替代 `tester.ios`;`builder.android` 同理。多平台运行不得使用无法表达平台的通用 `tester.test` 绑定。每一行都必须解析出独立的 tester Agent、Orca 环境、项目和路径。 完成矩阵后,按以下顺序检查**所有行**,但不执行测试用例: 1. 产物:调用方给出的文件或下载地址存在;Android APK 的 package、iOS IPA 的 Bundle ID 与 catalog 契约(若有)一致。 2. 执行目标:按 [execution.md](execution.md) 精确解析每个平台的 `tester` 绑定; 不允许把本机 worktree 当成远程 Mac 的 worktree,也不允许用旧 commit 的远程目录 代替本轮产物对应的项目目录。 3. 平台环境:Android 执行 adb、设备、APK、HOST 检查;iOS 在 macOS 执行 `uname`、`xcrun`/`simctl`、模拟器或真机、App 安装、HOST 检查。 4. 依赖环境:浏览器、权限、网络、测试账号或 catalog 明确要求的其它 session 前置。 所有检查完成后才能进入下方的 `select` 和 session 初始化。任一行不可用时,必须返回 完整清单并停止: ```text ## tester preflight:阻塞 目标产物: Android, iOS 可用: <平台 + 产物 + 环境> 不可用: - <平台>: <具体检查项和观测结果> 修复: <用户可执行动作> 验证: <可复制命令> 本轮未创建测试 session,未执行测试用例。 完成修复后重跑原 tester 命令。 ``` 不能因为 Android 可用就先测 Android,也不能用 API、另一平台或旧产物替代不可用行。 先按 `execution.md` 处理目标机器和项目目录,输出配置更新结果,再进入测试流程。目标测试项目必须包含 可读取的 `.pouch/tester/catalog.yaml`。父仓库可能包含多个测试子项目;运行前用 `catalog.py roots --root ` 发现全部目标根,不能把父仓库无 catalog 当成失败, 也不能偷偷回退到某一个子项目或复制测试文件。所有目标根都要被 fan-out 调度,每个根独立 做产物/环境 preflight,并单独生成运行快照。 `catalog.py select` 输出的 `agent` 是本轮使用的配置快照。记录中要保留 Agent、目标 环境、主机、项目、路径、worktree id、terminal handle,以及 terminal 是否成功关闭。 ## 1. 选范围 未指定范围 → `--tag smoke`,风险上限 = `default_max_risk`,`automation` 默认只跑 `automated`。 用户说分类、id、tag、风险上限时,原样传给 select: ```bash python3 /scripts/catalog.py select --root \ [--category CAT] [--id ID]... [--tag smoke|full|slow] \ [--max-risk ] [--driver script|browser|mobile] \ [--automation automated|assisted|manual|any] \ [--platform android|ios]... \ [--artifact android=] [--artifact ios=] \ [--binding android=tester.android] [--binding ios=tester.ios] ``` 选中任一 `driver=mobile` 用例时,`--platform`、每个平台的 `--artifact` 和对应 `tester` binding 是硬性参数;`catalog.py select` 会检查产物文件/下载地址和 `.pouch/agent.yaml` 中的 binding。缺任何一项直接返回非零并打印 `preflight: blocked`, 不能拿到 cases 后绕过闸门开始测试。默认 binding 是 `tester.`,所以 iOS 会强制检查 `tester.ios`,不会回退到 `builder.ios` 或通用 `tester.test`。 `--automation` 默认 `automated`(需人工的条目会被跳过,并在 JSON `skipped` 里说明);用户明确要求现场配合时用 `assisted`,`any` 表示不按人工参与度筛。 单根 `select` 的 JSON 字段:`max_risk` / `automation` / `count` / `vars`(已展开的变量,用来替换 `how` 里的 `${…}`)/ `artifacts` / `preflight` / `skipped` / `run_together` / `status_probe`(按分类,`url` 已展开)/ `cases`。从父仓库 fan-out 时返回 `mode: fan-out` 和每个根的独立结果,不能把多个根的 cases 拼成一个列表。 `select` 已排除 `retired` 和风险高于上限的条目(后者若用户点了具体 id,会在 JSON 的 `skipped` 里说明,不执行)。 若 catalog 有 `run_together`,命中组内任一条就把整组按 `ids` 顺序扩进列表(JSON 带 `run_together`)。这不是擅自扩大范围。 无命中:停下说明筛选条件,不要扩大范围。 ## 2. 运行前闸 - `HOST` 未设(环境变量 / `local.yaml` 都没有):全部停,environment-fail。 - 命中条的 `risk` 在 catalog `authorize` 里有对应变量:变量不是 `true` 则该条 `skipped`。有 `gate` 时还要 `catalog.gates` 对应变量为 `true`;`assume` 不成立同样 `skipped`。即使用户点了 id 也不执行。 - 授权/假设不成立是**主动不跑**(`skipped`),不是 `not-run`:这是我们决定不跑,不是没轮到。 - `status_probe` 是**分类级前置条件**(业界对应 pytest 的 session fixture / Robot 的 Suite Setup)。用 `select` 返回的 `status_probe` 解析生效值,不要自己读 catalog 猜。返回的 `probe.url` 已用 `vars` 展开,直接请求即可;`select` 同批返回 `vars`: | `categories[].status_probe` | 生效探测 | |------------------------------|----------| | 未声明 | 继承顶层 `catalog.status_probe`(`source: global`)| | `false` | 该分类不做闸(`source: disabled`)| | mapping | 整体覆盖顶层,不合并(`source: category`)| - 同一生效探测一次运行只请求一次,结果复用。 - 探测不可达或命中 `stop_if`:在 preflight 阶段记录对应分类/平台的不可用项并停止整轮; 先返回用户处理,不创建后续测试 session。只有所有本轮目标环境通过,才进入用例执行。 ## 2.1 移动端平台环境闸门与用户引导 `driver=mobile` 不是一个可以互换的平台。运行前必须根据 catalog session、用例变量或用户指定,明确本轮是 Android 还是 iOS;无法判断时先停下询问平台,不得猜测。两类平台分别执行以下闸门: ### Android 按顺序确认: 1. `adb` 可执行,且版本与当前 SDK 配置一致。 2. `adb devices -l` 至少有一个状态为 `device` 的目标;`unauthorized`、`offline` 或空列表都不能继续。多个目标时必须显式设置 `ANDROID_SERIAL`。 3. 调用方已给出本轮 APK 的本地路径或 URL。由 tester skill 取用或下载该文件,读取 APK 身份,自动执行 `adb install -r`,再用 `adb -s "$ANDROID_SERIAL" shell pm path ` 验证。 4. 测试所需的 HOST、端口、局域网/DNS 和权限可从设备访问。 ### iOS 按顺序确认: 1. 运行节点是 macOS,并且 `xcrun`、Xcode Command Line Tools 和 `simctl` 可用。Linux/Windows 节点不能伪装成 iOS 环境;需要 macOS 或可访问的远程 Mac。 2. 模拟器用 `xcrun simctl list devices available` 确认目标存在且已启动,再用 `xcrun simctl bootstatus -b` 确认完成启动;真机用 `xcrun xctrace list devices` 确认已连接、已信任且允许开发。 3. 调用方已给出本轮 IPA 的本地路径或 URL。由 tester skill 在 tester.ios 的 Mac 取用或下载该文件,解包读取 `Payload/*.app/Info.plist`,校验 Bundle ID,自动执行 `xcrun simctl install`,再用 `xcrun simctl get_app_container ` 验证。 4. 测试所需的 HOST、端口、局域网/DNS 和权限可从该模拟器/真机访问。 Android+iOS 默认是全量 preflight 闸门:任一项不满足,另一平台也不得安装或执行,并在记录中标为 `not-run`。只有用户显式加 `--partial` 才允许已通过的平台继续。不能改成 API 测试、不能把它们标成产品 `fail`,也不要继续初始化被闸门挡住的平台 session。记录实际探测命令及简短输出,不要把整段设备日志塞进对话。 任一项不满足,都要在 preflight 汇总中列出平台、具体检查项、实际探测命令和简短输出,然后停止整轮;只有 preflight 全部通过后,才把对应移动用例的结果写入运行快照。 ### 环境缺失时的用户引导 对话中必须输出一段可执行的引导,至少包含以下字段: ```text ## tester 环境引导:需要补齐 平台: Android | iOS 状态: environment-fail 缺失: <具体探测项和观测结果> 请完成: 1. <修复动作;不要只写“检查设备”> 2. <如需连接/启动/安装,写出动作> 验证: <一条或多条可复制的验证命令> 完成后: 回复“环境已就绪”,然后重跑原 tester 命令。 ``` 可按实际缺失替换为以下平台化提示: - Android 无设备:提示用户连接并解锁设备、开启 USB 调试、接受 RSA 授权;多个设备时设置 `ANDROID_SERIAL`;再运行 `adb devices -l`,直到目标状态为 `device`。 - Android 缺少产物位置:停下向调用方要本轮 APK 的本地路径或 URL,不要手工 scp 或沿用旧包。 - iOS 非 macOS 或无 Xcode:明确说明 iOS 测试需要 macOS + Xcode/Command Line Tools,提示用户切换到本机 Mac 或配置可访问的远程 Mac;不要建议在 Linux 上安装一个同名命令来绕过检查。 - iOS 模拟器未启动:提示用户在 Xcode 或 `simctl` 中创建/启动可用设备,使用 `xcrun simctl bootstatus -b` 验证。 - iOS 真机未就绪:提示用户连接数据线、解锁并信任此电脑、开启开发者模式并确认设备出现在 `xcrun xctrace list devices`。 以下是两个可复现的引导示例,测试 skill 行为时应得到同类结果: 1. Android:`adb devices -l` 只有 `List of devices attached`。期望状态为 `environment-fail`,引导连接/授权设备并给出 `adb devices -l` 验证命令。 2. iOS:Linux 节点没有 `xcrun`。期望状态为 `environment-fail`,引导切换到 macOS + Xcode 节点,并说明不能用 API 或 Android session 代替。 环境引导只负责让用户补齐前置条件;除 catalog 已明确授权的 session 初始化外,不自动安装 SDK、配对真机、修改系统安全设置或上传产物。 ## 3. Session 看命中条用到的 `session` 键: - 尚未初始化:按 catalog `sessions..init` 做。`reuse: persist` 的先 `probe`,活着就附着。 - 本轮已初始化:直接用,不要重开浏览器或模拟器。 - `reuse: run`:整轮结束再关。`persist`:不关。 - 中途 session 挂了:后续同 driver 标 environment-fail,不要标产品失败。 ## 4. 执行 按 select 返回的顺序,一条一条来。select 按 `catalog.yaml` 的 `categories` 顺序,同一分类内按该 yaml 列表顺序(不是文件名字母序)。默认失败即停(产品 `fail` 或探测到的不安全状态)。用户说继续 / keep-going 才往后跑。 例外:当前条和下一条同属 `run_together` 且该组 `continue_on_fail: true` 时,前一条产品 `fail` 仍跑后一条。`environment-fail` 仍整轮停。 **automation** - `automated`:Agent 端到端执行。 - `assisted`:照常驱动,但遇到以 `[人工]` 开头的 `how.steps` 时停下,请现场人员执行并回报,确认后再继续。无人值守时该条记 `not-run`。 - `manual`:没有 `driver`,`run-script` 会直接返回 `not-run`。只能由人执行、回报结果,Agent 负责记录。 **script** 有 `how.run`: ```bash python3 /scripts/catalog.py run-script --root --id ``` 这个命令必须发送到本轮创建的 terminal 中执行;`` 是目标机器上的项目根, 不是发起端当前目录。远程目标上的 skill 脚本不可用时,直接在该 terminal 中执行 `how.run` 展开后的命令,并保留同样的 stdout/exit 证据。`run-script` 只跑命令,返回 `executed` + stdout/exit(或 skipped / environment-fail)。**pass/fail 仍由 Agent 对照 `expected` 判定**,不要把 curl 退出码 0 当成产品通过。 没有 `run`:按 `how.steps` 做,证据对照 `expected`。不要发明 steps 里没有的调用。 **browser / mobile** 挂到已有 session,执行 `how.steps`,对照 `expected`。必须留下可见文本、URL 或交互证据。 ## 5. 记录与报告 每条:`id, result, evidence, notes`。`result` 只能是 `pass` / `fail` / `environment-fail` / `skipped` / `not-run`。`skipped` 是主动不跑(retired / pending / 超风险上限 / 未授权 / `assume` 不成立),`not-run` 是没轮到(失败即停后剩下的 / 被前置整组挡住的 / 需人工而无人值守的)。 写入 `.pouch/tester/runs/-<范围>-<随机后缀>.yaml`,可用 `generate_run_csv.new_run_id(<范围>)` 生成同名 `runId`。记录包含唯一 `runId`、筛选条件、HOST 是否已设(可以写 HOST 值)、session 是否复用、每个分类的 `status_probe` 生效值与观测结果(`source` / `disabled` 原因 / `hit` / `observed`)、逐条结果。记录文件只创建一次,不覆盖已有文件。立刻用同一快照生成 CSV,不要手改: ```bash python3 /scripts/generate_run_csv.py --root --run ``` 这会写出与快照同名的 `.pouch/tester/runs/.csv`(`executed=yes|no`),不会修改 `cases.csv`。对话里两个路径都报。 对话报告: ```text ## tester 运行:完成 | 部分完成 | 阻塞 范围: … 风险上限: … session: browser=复用|新开|未用;mobile=… Agent: / / 执行目标: /:: terminal: 新建 ,已关闭|清理失败 计数: pass=N fail=N environment-fail=N skipped=N not-run=N 失败: id + 一句证据 跳过: id + 原因 快照: .pouch/tester/runs/.yaml 成绩单: .pouch/tester/runs/.csv 下一步: 一句话 ```