返回 CodeWhale
README.md
根目录 / crates / tui / tests / support / qa_harness / README.md
1 # PTY/frame-capture TUI QA harness
2
3 Tiny helper for integration tests that need to drive `deepseek-tui` like a real
4 user typing in a real terminal — keys and paste, plus assertions over the
5 parsed terminal frame and the workspace filesystem.
6
7 ## When to use this
8
9 Reach for this harness when a bug only shows up in the **interactive**
10 terminal: paste behaviour, slash menus, mode switching, viewport rendering,
11 onboarding flow, mouse capture. Anything where a `TestBackend` or a
12 unit test on the underlying state machine is too divorced from what the user
13 actually sees.
14
15 For pure logic tests on `App`, `SkillRegistry`, the engine's `Op` / `Event`
16 plumbing, etc., keep using `crates/tui/src/.../tests` style unit tests. Don't
17 spin up a PTY just to assert a function returns the right value.
18
19 ## Anatomy
20
21 - `pty.rs` — `PtySession`. Spawns a binary in a real PTY (via `portable-pty`),
22 pumps the child's stdout into a buffer on a background thread, exposes
23 `write_bytes`, `drain`, `shutdown`.
24 - `frame.rs` — `Frame`. Wraps `rio-vt`. Feed bytes in, ask questions
25 out: `text()`, `row(y)`, `contains(s)`, `cursor()`, `debug_dump()`.
26 - `keys.rs` — byte-sequence builders for keys (`key::ch('/')`,
27 `key::enter()`, `key::text("hello")`) and for paste (`paste::bracketed(s)`,
28 `paste::unbracketed(s)`).
29 - `harness.rs` — `Harness`. Composes the two. Has `wait_for`, `wait_for_text`,
30 `wait_for_idle`, plus `make_sealed_workspace()` for a tempdir HOME.
31
32 ## Adding a new scenario
33
34 1. Pick the smallest set of inputs that reproduce the user-visible behaviour.
35 If you can't reproduce it without a real LLM turn, the scenario probably
36 belongs in a unit test (or a `wiremock`-driven turn test) instead.
37
38 2. Build a sealed workspace so the scenario doesn't see the developer's real
39 `~/.deepseek/` or API keys:
40
41 ```rust
42 let ws = qa_harness::harness::make_sealed_workspace()?;
43 std::fs::write(ws.user_skills_dir().join("foo/SKILL.md"), "...")?;
44 ```
45
46 3. Spawn:
47
48 ```rust
49 let mut h = Harness::builder(Harness::cargo_bin("codewhale-tui"))
50 .cwd(ws.workspace())
51 .seal_home(ws.home())
52 .env("DEEPSEEK_API_KEY", "ci-test-key")
53 .args(["--workspace", ws.workspace().to_str().unwrap(),
54 "--no-project-config", "--skip-onboarding"])
55 .size(40, 120)
56 .spawn()?;
57 ```
58
59 4. Drive it:
60
61 ```rust
62 h.wait_for_text("Composer", Duration::from_secs(10))?;
63 h.send(keys::key::ch('/'))?;
64 h.wait_for_text("/skills", Duration::from_secs(2))?;
65 ```
66
67 5. Assert:
68
69 ```rust
70 let f = h.frame();
71 assert!(f.contains("local-skill"), "frame:\n{}", f.debug_dump());
72 ```
73
74 6. Always shut down cleanly at the end so the PTY cleanup runs even on a
75 failing assertion:
76
77 ```rust
78 let _ = h.shutdown();
79 ```
80
81 ## Conventions
82
83 - **Sealed env always.** No scenario should be able to see the real
84 `$HOME/.deepseek/` or contact `api.deepseek.com`. If a scenario *has* to do a
85 real model turn, route through a local `wiremock` or `tiny_http` fake
86 provider and pass `DEEPSEEK_BASE_URL=<localhost>`.
87 - **Fail noisily.** When an assertion fails, print `frame.debug_dump()` so the
88 CI log shows the rendered screen, not just `assertion failed`.
89 - **Prefer `wait_for_text` over `sleep`.** A scenario that sleeps 500ms before
90 asserting will flake under CI load. A scenario that polls with a 10s
91 timeout is robust.
92 - **Expect output to be slow on first launch.** The TUI does config probing,
93 skill installation, and snapshot cleanup before showing the composer.
94 Give startup at least 10–15 seconds before timing out.
95
96 ## Platforms
97
98 `portable-pty` works on macOS, Linux, and Windows (ConPTY). Today the
99 scenarios target Unix only — the test binary is gated with
100 `#![cfg(unix)]` until the Windows-specific input plumbing has been audited
101 under the same harness.
102
102 lines MARKDOWN