#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::string_slice,
clippy::unwrap_in_result,
reason = "integration test code; panics are assertion failures"
)]
use std::io::{Read, Write};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use koh::server::run_session;
use koh::transport_iroh::{bind_endpoint_local, format_endpoint_id, generate_secret_key};
use portable_pty::{native_pty_system, CommandBuilder, PtySize};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn real_client_binary_renders_over_pty() {
let server_ep = bind_endpoint_local(generate_secret_key(), true)
.await
.expect("bind server");
let server_id = format_endpoint_id(&server_ep.id());
let server_port = server_ep
.bound_sockets()
.iter()
.find(|s| s.is_ipv4())
.map(std::net::SocketAddr::port)
.expect("server v4 port");
let server_task = tokio::spawn(async move {
if let Some(incoming) = server_ep.accept().await {
if let Ok(conn) = incoming.await {
if koh::transport_iroh::auth::handshake_server(&conn, None)
.await
.is_ok()
{
let _ = run_session(conn, Some("sh".into()), 0).await;
}
}
}
});
let key_path = std::env::temp_dir().join(format!("koh-pty-test-{}.key", std::process::id()));
let _ = std::fs::remove_file(&key_path);
let pty = native_pty_system();
let pair = pty
.openpty(PtySize {
rows: 24,
cols: 80,
pixel_width: 0,
pixel_height: 0,
})
.expect("openpty");
let mut cmd = CommandBuilder::new(env!("CARGO_BIN_EXE_koh"));
cmd.arg("connect");
cmd.arg(&server_id);
cmd.arg("--direct");
cmd.arg(format!("127.0.0.1:{server_port}"));
cmd.arg("--predict");
cmd.arg("never");
cmd.arg("--key-file");
cmd.arg(&key_path);
cmd.env("TERM", "xterm-256color");
let mut child = pair.slave.spawn_command(cmd).expect("spawn client binary");
drop(pair.slave);
let mut reader = pair.master.try_clone_reader().expect("clone reader");
let buf = Arc::new(Mutex::new(Vec::<u8>::new()));
let buf_reader = buf.clone();
std::thread::spawn(move || {
let mut tmp = [0u8; 8192];
loop {
match reader.read(&mut tmp) {
Ok(0) | Err(_) => break,
Ok(n) => buf_reader.lock().unwrap().extend_from_slice(&tmp[..n]),
}
}
});
let mut writer = pair.master.take_writer().expect("take writer");
tokio::time::sleep(Duration::from_millis(2000)).await;
writer
.write_all(b"echo koh_pty_marker\r")
.expect("write keystrokes");
writer.flush().ok();
let contains_marker = |b: &Arc<Mutex<Vec<u8>>>| {
String::from_utf8_lossy(&b.lock().unwrap()).contains("koh_pty_marker")
};
let mut seen = false;
for _ in 0..150 {
if contains_marker(&buf) {
seen = true;
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
let _ = writer.write_all(&[0x1e, b'.']);
let _ = writer.flush();
tokio::time::sleep(Duration::from_millis(300)).await;
let _ = child.kill();
server_task.abort();
let _ = std::fs::remove_file(&key_path);
let rendered = String::from_utf8_lossy(&buf.lock().unwrap()).to_string();
assert!(
seen,
"real client binary never rendered the marker over the PTY; captured:\n{}",
&rendered[rendered.len().saturating_sub(2000)..]
);
}