Add mock-claude fixture, test_pty_spawns_tty integration test, and hook module export

- test-fixtures/mock-claude: implement mock binary that writes 'stop' to FIFO then checks isatty(stdin), exiting 0 on TTY
- tests/pty_integration.rs: test_pty_spawns_tty uses HookInstaller + PtySpawner to verify controlling TTY is established
- src/lib.rs: expose hook module publicly
- Cargo.lock: add libc dependency for mock-claude

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-06-08 08:56:36 -04:00
parent a1e74b59fa
commit 17c35f40a7
5 changed files with 71 additions and 1 deletions

3
Cargo.lock generated
View file

@ -262,6 +262,9 @@ checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "mock-claude"
version = "0.1.0"
dependencies = [
"libc",
]
[[package]]
name = "nix"

View file

@ -1,4 +1,5 @@
pub mod cli;
pub mod config;
pub mod error;
pub mod hook;
pub mod pty;

View file

@ -6,3 +6,6 @@ edition = "2021"
[[bin]]
name = "mock-claude"
path = "src/main.rs"
[dependencies]
libc = "0.2"

View file

@ -1 +1,17 @@
fn main() {}
use std::io::Write;
fn main() {
let fifo_path = std::env::args()
.nth(1)
.expect("usage: mock-claude <fifo-path>");
// Simulate the stop hook by writing "stop" to the FIFO.
// O_WRONLY on a FIFO blocks until a reader opens the other end.
if let Ok(mut file) = std::fs::OpenOptions::new().write(true).open(&fifo_path) {
let _ = file.write_all(b"stop\n");
}
// Exit 0 if stdin is a controlling TTY (login_tty succeeded), 1 otherwise.
let has_tty = unsafe { libc::isatty(libc::STDIN_FILENO) } == 1;
std::process::exit(if has_tty { 0 } else { 1 });
}

47
tests/pty_integration.rs Normal file
View file

@ -0,0 +1,47 @@
use claude_print::hook::HookInstaller;
use claude_print::pty::PtySpawner;
use std::ffi::CString;
use std::io::Read;
/// Locate the mock-claude binary compiled alongside the test binary.
/// Test binaries live at `target/<profile>/deps/`; other bins at `target/<profile>/`.
fn mock_claude_bin() -> std::path::PathBuf {
let exe = std::env::current_exe().expect("current_exe");
let profile_dir = exe
.parent() // deps/
.and_then(|p| p.parent()) // target/<profile>/
.expect("unexpected test binary path");
profile_dir.join("mock-claude")
}
#[test]
fn test_pty_spawns_tty() {
let installer = HookInstaller::new().expect("HookInstaller::new");
let fifo_path = installer.fifo_path.clone();
// Read from the FIFO in a background thread so mock-claude's write doesn't block.
let reader = std::thread::spawn(move || {
let mut file = std::fs::File::open(&fifo_path).expect("open stop.fifo");
let mut buf = String::new();
file.read_to_string(&mut buf).ok();
buf
});
let bin = mock_claude_bin();
let cmd = CString::new(bin.to_str().expect("binary path is utf-8")).expect("CString");
let fifo_arg =
CString::new(installer.fifo_path.to_str().expect("fifo path is utf-8")).expect("CString");
let spawner = PtySpawner::spawn(&cmd, &[fifo_arg]).expect("PtySpawner::spawn");
let exit_code = spawner.relay().expect("relay");
let fifo_content = reader.join().expect("reader thread");
assert!(
fifo_content.contains("stop"),
"expected 'stop' in FIFO content, got: {fifo_content:?}",
);
assert_eq!(
exit_code, 0,
"mock-claude should detect a controlling TTY (isatty(0) returned false)",
);
}