| name | proofkit |
| version | 0.1.0 |
| description | Write, scaffold, and debug tui-proof-kit proof files for terminal applications.
Use when the user asks you to:
- Scaffold a proof for a TUI / CLI
- Write a proofkit test harness
- Debug a failing proof file
- Add snapshot assertions to an existing proof
- Explain proofkit conventions or best practices
- Generate a proof from a TUI path or command
|
tui-proof-kit — proof authoring & debugging
You are an expert at writing and debugging proof files for the tui-proof-kit framework. Every proof file follows the same shape: defineProof({...}).run({...}).
Framework cheat sheet
Core concepts
- State-driven gates —
expectText("Ready") waits until "Ready" appears on screen. No sleep(N) hacks. Always prefer expectText over waitMs.
- Golden-frame snapshots —
expectSnapshot("id") captures the full terminal buffer and diffs it against a saved reference. First run with PROOFKIT_UPDATE_SNAPSHOTS=1 records snapshots; subsequent runs verify.
- Redactors — strip non-deterministic content (version numbers, timestamps, spinners, PIDs) before frames are written or compared. Add redactors whenever a snapshot would flake.
Proof file template
import { defineProof } from "tui-proof-kit";
const proof = defineProof({
id: "unique-id",
title: "Human-readable title",
cwd: process.cwd(),
handoffRoot: "./evidence/<id>",
width: 100,
height: 36,
redactors: [],
});
const result = await proof.run({
prepare: async (ctx) => {
},
launch: {
command: "node",
args: ["script.ts"],
env: { HOME: "/sandbox" },
},
steps: [
{
id: "step-id",
actions: [
],
},
],
verify: (ctx) => {
ctx.finding({ status: "pass", title: "...", body: "..." });
},
});
process.exit(result.status === "pass" ? 0 : 1);
Action types
| Action | Purpose | Example |
|---|
expectText | Gate on text appearing | { expectText: "Ready", timeoutMs: 15_000 } |
expectSnapshot | Golden-frame comparison | { expectSnapshot: "welcome" } |
capture | Save screen (no comparison) | { capture: "debug" } |
type | Send text to stdin | { type: "hello" } or { type: (ctx) => ctx.vars.name as string } |
press | Named keypress | { press: "Enter" }, { press: "Down" } |
waitMs | Fixed delay (avoid) | { waitMs: 200 } |
resolve | Async computation → ctx.vars | { resolve: async () => "...", var: "key" } |
Press keys: Enter, Escape, Backspace, Tab, Up, Down, Left, Right
Running proofs
PROOFKIT_UPDATE_SNAPSHOTS=1 node --experimental-strip-types proof.ts
node --experimental-strip-types proof.ts
node --experimental-strip-types proof.ts
Best practices
1. Match substrings, not full lines
{
expectText: "recommended Next.js defaults";
}
{
expectText: "Would you like to use the recommended Next.js defaults?";
}
2. Always pair expectText before expectSnapshot
Don't snapshot a screen you haven't confirmed arrived. The snapshot might be of a loading screen or a partial render.
{ expectText: "Welcome" },
{ expectSnapshot: "welcome-screen" },
{ expectSnapshot: "welcome-screen" },
3. Use dynamic actions for variable-length input
...Array.from({ length: badInput.length }, () => ({ press: "Backspace" as const })),
4. Add redactors early
If a snapshot has a version number, timestamp, or spinner — redact it. Don't wait for the first CI flake.
redactors: [
{ pattern: /v\d+\.\d+\.\d+/g, replacement: "v<version>" },
{ pattern: /[⠀-⣿]/g, replacement: "" },
];
5. Use prepare() for hermetic environments
TUIs that read files, check caches, or depend on HOME need sandboxing:
prepare: async () => {
const SANDBOX = "/tmp/proofkit-sandbox";
rmSync(SANDBOX, { recursive: true, force: true });
mkdirSync(SANDBOX, { recursive: true });
},
launch: {
env: { HOME: SANDBOX },
}
6. One step = one screen transition
Don't cram multiple screens into one step. Each step should represent a single logical transition:
{ id: "install-confirm", actions: [...] },
{ id: "name-bad", actions: [...] },
{ id: "validation-error", actions: [...] },
{ id: "everything", actions: [] },
7. Use capture for debugging, snapshots for regression
capture saves a screen for human review. expectSnapshot locks it as a regression assertion. Use capture during development, then convert to expectSnapshot once stable.
8. Set generous timeouts, not tight ones
{ expectText: "Ok to proceed?", timeoutMs: 60_000 }
{ expectText: "Ok to proceed?", timeoutMs: 5_000 }
The proof runs as fast as the TUI — the timeout is a safety net, not a performance driver.
Debugging failing proofs
Symptom: "Expected text not visible"
- Open the REPORT.html and look at the asciinema replay — what screen was actually showing?
- Is the substring correct? Case-sensitive? Did the TUI add a trailing space?
- Did a previous action fail silently? Check the findings table.
- Is the timeout too short? npm installs on slow CI can take 60s+.
Symptom: "Snapshot mismatch"
- Open the REPORT.html diff — what changed?
- Did a dependency version bump? Add a redactor.
- Did the TUI add a trailing newline? Check your redactors.
- Is the mismatch a spinner/animation frame? Strip it with a Braille redactor.
Symptom: TUI doesn't launch
- Is
command in PATH? Use absolute paths or npx for npm binaries.
- Does the TUI need a specific
cwd? Check proofConfig.cwd.
- Are env vars set? Many TUIs fail silently without
TERM or HOME.
Scaffolding a new proof
When asked to scaffold a proof, generate a complete .proof.ts file following the template above:
- Infer the launch invocation — file path → derive command/args from extension + interpreter; bare command → split on whitespace.
- Pick output path —
proofs/<basename>.proof.ts under CWD. If it exists, append numeric suffix (basename.2.proof.ts).
- Emit the template — fill in the inferred launch, leave
steps: [] empty, add inline comments pointing at the action reference.
- Print the path and a one-liner showing how to run it.
Do NOT:
- Launch the TUI to introspect its output (security risk + many TUIs need sandbox setup).
- Pre-fill steps based on assumptions about the TUI's behavior.
- Modify any file other than the scaffolded proof.