# 真实 Pi TUI 验收

仓库提供一条可重复的真实 TUI 验收命令。它启动仓库锁定的 Pi、`pi-video-tui` extension、真实 native sidecar 和同一个 libmpv 音视频实例，并通过 PTY 驱动输入与 terminal resize。命令同时启动本地确定性 streaming provider，最后把 ANSI、layout、native 和 cleanup 结果写成 JSON 报告。

该流程不读取或修改用户真实 Pi 配置，不提交媒体 fixture，也不替代 TypeScript/Rust 自动化测试。

## 快速开始

先构建 native sidecar，再显式提供调用者拥有的媒体：

```bash
npm run build:native
npm run acceptance:tui -- \
  --video "/absolute/path/含 空格/video.mp4" \
  --scenario baseline
```

运行所有场景：

```bash
npm run acceptance:tui -- \
  --video "/absolute/path/video.mp4" \
  --scenario all
```

成功时命令退出 0；preflight、action、assertion 或 cleanup 任一失败时退出非 0。终端会显示简短 PASS/FAIL 摘要，完整机器判定写入报告。默认 artifact 目录是：

```text
.artifacts/pi-video-tui-acceptance-<timestamp>-<pid>/
```

可用 `--artifacts <directory>` 固定目录，或用 `--report <path>` 单独指定聚合报告。

## Host prerequisites

命令在进入 TUI 前逐项检查：

- Node.js `>=22.19.0`；
- Python 3，并可导入 POSIX `pty`、`fcntl` 和 `termios`；
- 仓库 `node_modules/.bin/pi` 与 `@earendil-works/pi-tui` 都是 `0.81.1`；
- `pkg-config --modversion mpv` 可解析 libmpv；
- `native/target/release/pi-video-tui-native` 存在、可执行并能由动态加载器装载；
- `--video` 指向可读普通文件。

默认环境基线是 Apple Silicon macOS、Homebrew mpv 和 Rust 构建出的 release sidecar。`--pi <path>` 与 `--native <path>` 可覆盖可执行文件，但 Pi/Pi TUI 版本 gate 不会放宽。

缺少任一依赖时不会产生误导性通过。命令会退出非 0，并生成 `phase: "preflight"`、失败检查项和原始诊断。

## 隔离与真实进程链

每个场景拥有独立目录，命令只在该目录写入：

- `agent/models.json`：指向 loopback 端口的 OpenAI-compatible provider；
- `agent/settings.json`：固定 model、theme、SSE transport、关闭 retry 和 project trust；
- `agent/pi-video-tui.json`：显式媒体路径、`maxHeightPercent: 50`、音量 0 和 native proxy；
- 独立 session、trace 和 report 文件。

Pi 使用 `PI_CODING_AGENT_DIR`、`PI_CODING_AGENT_SESSION_DIR`、`PI_OFFLINE=1`、`--no-approve` 和所有 `--no-*` resource flags 启动，只通过 `--extension <repo>/src/index.ts` 加载被测 extension。用户的 auth、models、settings、extensions、skills、prompts、themes 和 sessions 都不会被读取或修改。

native proxy 是字节透明的测试 Adapter。它只启动一个真实 sidecar，原样转发 stdin、PIV1 stdout 和 stderr，同时旁路记录 start、READY、FRAME、resize、END 和 exit。它不解码 framebuffer、不重建 PIV1 消息，也不创建第二个 mpv handle。

媒体参数使用 argv 和 JSON 传递，不经过 shell 拼接。中文、空格和符号路径会原样到达真实 sidecar。

`widget-observer.ts` 在目标 extension 前加载，透明包装同一个真实 `ctx.ui.setWidget`，记录目标 key 的 mount/remove 调用后原样转发。报告因此同时验证 Pi Widget registry 的真实移除和终端画面的视觉移除；Ctrl+D 在进程退出后无法再观察 redraw，仅使用 registry trace 证明 shutdown cleanup。

## 场景

| 场景 | 主要行为与判定 |
| --- | --- |
| `baseline` | 默认 `120x40`；提交包含 `q` 的消息；等待 mixed 纯文本/Markdown reply；验证 conversation、完整视频行、editor、footer 顺序；流式阶段 0 full-screen clear、0 native resize；Escape 后移除 Widget。 |
| `resize` | 播放中从初始尺寸 resize 到 `160x50`；区分 1 次预期 full redraw/resize 与稳定后的持续重绘；验证新 geometry 后帧继续推进。 |
| `editor-wrap` | 输入 500+ 普通字符并提交；只在 editor 实际折行和恢复时 resize；确认完整 `q` 消息到达 provider。 |
| `eof` | 不发送跳过键，等待真实媒体自然 EOF；适合短媒体。 |
| `ctrl-c` | editor 中保留包含 `q` 的文字；第一次 Ctrl+C 只关闭视频；随后 Enter 提交原文字。 |
| `ctrl-d` | 空 editor 播放中发送 Ctrl+D，验证 Pi shutdown 和 sidecar cleanup。 |
| `ghostty` | 设置 `TERM_PROGRAM=ghostty`；播放期验证 DEC 2026 marker 和视频行 `CSI 2K` 均被抑制；关闭后验证 writer 恢复。 |
| `all` | 按上表顺序独立运行全部场景，每个场景重新创建 Pi、provider、PTY 和 sidecar。 |

`eof` 的 deadline 必须大于媒体时长；必要时使用 `--eof-timeout-ms`。短于 baseline/provider stream 的媒体可能先自然结束，应改用更长媒体验证 Escape、resize 和 streaming 场景。

确定性 provider 支持 `--response plain|markdown|mixed`，默认 `mixed`。它按固定 token 序列流式发送普通文本、正常 Markdown 和 `ACCEPTANCE_REPLY_TAIL`，并在 server-side trace 中记录最新 user message，用于确认 `q` 没有被 extension 消费。

## PTY 与 timeout

公开 terminal 参数：

```text
--columns <positive integer>      默认 120
--rows <positive integer>         默认 40
--timeout-ms <positive integer>   普通 action 默认 15000
--eof-timeout-ms <positive integer>  EOF 默认 120000
```

PTY Adapter 支持 UTF-8 普通文字、`q`、Enter、Escape、Ctrl+C、Ctrl+D 和运行中 `TIOCSWINSZ`/`SIGWINCH`。每个 wait 都有独立 deadline，整个 plan 另有总 deadline。失败、父进程 signal 或 helper timeout 都会先 TERM、再 KILL 本次 PTY 拥有的 process group；如果 helper 本身卡死并被 supervisor 强杀，Node supervisor 会按 driver trace 中的 PGID 再做一层 TERM/KILL。报告只检查本次记录的 PID，不使用全局 `pgrep`，因此不会把其他 Pi/native 会话算成残留。

## 报告

聚合 `report.json` 包含 preflight 和所有场景。每个场景另有独立 `report.json`，主要字段包括：

- `status`、`scenario`、`assertions[]`；
- `terminal.initial/final/resizeEvents`；
- `ansi.fullScreenClears`、streaming/resize/stable phase clear；
- Ghostty 播放期 marker、video-line erase 和关闭后 marker；
- `layout` 中 conversation/video/editor/footer 可见性、顺序，以及连续完整/partial 视频行；
- `video` 中 native frame、不同 sequence/dimension、stream/resize 后 native 与 screen phase frame，以及 Widget mount/removal；
- `native` 中 start、initial box、resize commands、END reason、exit count 和 exit code；
- `input` 中 provider 收到的消息与 reply tail；
- `exit`、`cleanup.remainingProcesses` 和 `evidence` phase/trace 完整性。

依赖 checkpoint 的计数在证据缺失时为 `null`，对应 `evidence` 为 `false`，场景 assertion 必然失败；缺失 phase 不会按 0 产生伪通过。任一 JSONL 解析错误、native proxy/protocol error event 或 nonzero native exit code 同样直接使 verdict 失败。

同目录保留以下诊断资产：

```text
driver-plan.json   PTY argv/env/action plan
driver.jsonl       output offset、resize、checkpoint、exit
native.jsonl       proxy/real sidecar/PIV1/control trace
provider.jsonl     local request/response lifecycle
widget.jsonl       真实 Pi setWidget mount/remove lifecycle
pty.raw            完整 PTY bytes
tui.log            Pi ProcessTerminal.write bytes
driver.stderr.log  helper failure诊断
```

`pty.raw` 是完整进程输出的依据；`tui.log` 用于精确划分 Pi renderer phase。ANSI screen 由 `@xterm/headless` 重放；目标 phase 必须同时出现多个 screen signature 和多个真实 PIV1 sequence，任一侧冻结都会失败。Provider cleanup 会等待所有异步 request handler 结束后再写 `closed`，因此 verdict 读取后不会继续追加迟到的 error event。

## Ghostty 权限边界

`ghostty` 场景不启动额外 GUI 窗口。它验证 Pi terminal writer 在 `TERM_PROGRAM=ghostty` 下的实际输出契约，因此不需要 macOS Automation 或 Accessibility 权限。

真实 Ghostty Metal 窗口截图属于额外人工证据。只有执行主机已授予外部脚本启动应用和辅助功能权限时才要求；权限缺失不会让 writer trace 产生伪通过，也不会被报告成窗口截图成功。

## 聚焦自动化测试

不依赖真实媒体的 parser、provider、scenario 和 helper 回归：

```bash
npm test -- --run \
  test/acceptance-analyzer.test.ts \
  test/acceptance-harness.test.ts
```

这些测试覆盖 ANSI phase 分类、headless layout 重放、screen/native 双重推进、Widget lifecycle、报告 verdict、OpenAI SSE 与 handler drain、场景输入计划、PTY resize/input、PIV1 proxy 字节透明性，以及 action/supervisor timeout 后的 process-group cleanup。
