Skip to content

TUI E2E Testing

Crucible includes an expectrl-based test harness for end-to-end TUI testing. This enables PTY-based testing with real terminal emulation, supporting multi-turn interaction verification.

The test harness spawns the cru binary in a pseudo-terminal (PTY), allowing tests to:

  • Send keystrokes and text input
  • Wait for expected output patterns
  • Verify UI behavior across multi-turn interactions
  • Test the full TUI stack (rendering, input handling, agent communication)
+------------------+
| Test Case |
| (Rust test) |
+--------+---------+
|
v
+------------------+
| TuiTestSession |-- Spawns `cru chat` in PTY
| (expectrl) |-- Sends keystrokes
| |-- Captures output
+--------+---------+
|
v
+------------------+
| Assertions |-- Pattern matching
| |-- Regex expectations
+------------------+

Build the binary first (tests require a compiled cru):

Terminal window
cargo build --release

Run the TUI e2e tests (they’re ignored by default since they require a built binary):

Terminal window
cargo test -p crucible-cli tui_e2e -- --ignored

Run a specific test:

Terminal window
cargo test -p crucible-cli smoke_help -- --ignored

Configuration for test sessions:

let config = TuiTestConfig {
subcommand: "chat".to_string(),
args: vec!["--no-splash".to_string()],
env: vec![("MY_VAR".to_string(), "value".to_string())],
timeout: Duration::from_secs(10),
cols: 80,
rows: 24,
..Default::default()
};

Fluent builder for test sessions:

let mut session = TuiTestBuilder::new()
.command("chat")
.timeout(15)
.env("DEBUG", "1")
.spawn()
.expect("Failed to spawn");

The main session type for interacting with the TUI:

MethodDescription
spawn(config)Create session with config
spawn_chat()Create session for cru chat
expect(pattern)Wait for text pattern
expect_regex(pattern)Wait for regex pattern
send(text)Send raw text
send_line(text)Send text with Enter
send_key(key)Send special key
send_control(char)Send Ctrl+char
expect_eof()Wait for process exit
wait(duration)Fixed delay
output_contains(pattern)Non-blocking check

Special keys for TUI interaction:

pub enum Key {
Up, Down, Left, Right,
Enter, Escape, Tab,
Backspace, Delete,
Home, End,
PageUp, PageDown,
F(u8), // F1-F12
}
#[test]
#[ignore = "requires built binary"]
fn my_tui_test() {
let mut session = TuiTestBuilder::new()
.command("chat")
.timeout(10)
.spawn()
.expect("Failed to spawn");
// Wait for initial render
session.wait(Duration::from_secs(1));
// Interact with TUI
session.send("Hello").expect("Failed to send");
session.send_key(Key::Enter).expect("Failed to send Enter");
// Verify behavior
session.wait(Duration::from_millis(500));
// Clean exit
session.send_control('c').expect("Failed to exit");
}
#[test]
#[ignore = "requires built binary"]
fn test_help_command() {
let mut session = TuiTestBuilder::new()
.command("chat")
.timeout(10)
.spawn()
.expect("Failed to spawn");
session.wait(Duration::from_secs(1));
// Trigger command popup
session.send("/").expect("Failed to send /");
session.wait(Duration::from_millis(200));
// Navigate and select
session.send("help").expect("Failed to type help");
session.send_key(Key::Enter).expect("Failed to confirm");
// Clean exit
session.send_control('c').expect("Failed to exit");
}

For tests that require actual agent responses:

#[test]
#[ignore = "requires built binary and ACP agent"]
fn test_conversation() {
let mut session = TuiTestBuilder::new()
.command("chat")
.timeout(30) // Longer for LLM responses
.spawn()
.expect("Failed to spawn");
session.wait(Duration::from_secs(2));
// Turn 1
session.send_line("Say hello").expect("Failed to send");
session.wait(Duration::from_secs(10));
// Turn 2
session.send_line("Now say goodbye").expect("Failed to send");
session.wait(Duration::from_secs(10));
session.send_control('c').expect("Failed to exit");
}

The test suite is organized into categories:

CategoryPurposeExamples
SmokeBasic startup/shutdownsmoke_version, smoke_help
NavigationKey sequences, popupschat_popup_navigation
InputText entry, backspacechat_input_typing
CommandsSlash command behaviorchat_help_command
Multi-turnFull conversation flowschat_multiturn_basic
StressRapid input handlingchat_rapid_input

The harness is designed to support future enhancements:

The OutputChunk type captures output with timestamps for:

  • Flicker detection - Identify rapid redraws
  • Frame diffing - Compare render states
  • Performance profiling - Measure render latency

Future versions may integrate VTE parsing for:

  • Escape sequence analysis
  • Cursor position tracking
  • Color/style verification

Start with smoke tests: Verify basic --help and --version work before testing the full TUI.

Use generous timeouts: Agent responses vary in timing. Use 10-30 second timeouts for conversation tests.

Clean exit: Always send Ctrl+C at the end of tests to avoid orphaned processes.

Mark tests as ignored: TUI tests require a built binary and may need infrastructure. Use #[ignore = "reason"].

Test incrementally: Build up complex interactions step by step, verifying each stage.

FilePurpose
crates/crucible-cli/tests/tui_e2e_harness.rsTest harness implementation
crates/crucible-cli/tests/tui_e2e_tests.rsTest cases