mod common;
use std::sync::Arc;
use std::time::{Duration, Instant};
use common::mitm_stdin::{skip_unless_mitm_supported, EchoerSession};
use running_process::pty::backend::PtySize;
const RECEIVE_TIMEOUT: Duration = Duration::from_secs(10);
const PASTE_OPEN: &[u8] = b"\x1b[200~";
const PASTE_CLOSE: &[u8] = b"\x1b[201~";
macro_rules! skip_if_unsupported {
() => {
if skip_unless_mitm_supported() {
return;
}
};
}
fn wrap_paste(payload: &[u8]) -> Vec<u8> {
let mut v = Vec::with_capacity(PASTE_OPEN.len() + payload.len() + PASTE_CLOSE.len());
v.extend_from_slice(PASTE_OPEN);
v.extend_from_slice(payload);
v.extend_from_slice(PASTE_CLOSE);
v
}
#[test]
fn small_ascii_paste_round_trips_byte_exact() {
skip_if_unsupported!();
let session = EchoerSession::spawn(&[]);
let wrapped = wrap_paste(b"hello world");
session.write_stdin(&wrapped);
session.assert_received_exact(&wrapped, RECEIVE_TIMEOUT);
}
#[test]
fn one_megabyte_paste_survives_round_trip() {
skip_if_unsupported!();
let session = EchoerSession::spawn(&[]);
let payload = vec![0xABu8; 1_048_576];
let wrapped = wrap_paste(&payload);
session.write_stdin(&wrapped);
let got = session.drain_for(Duration::from_secs(30), Some(wrapped.len()));
assert_eq!(
got.len(),
wrapped.len(),
"expected {} bytes, got {}",
wrapped.len(),
got.len()
);
assert!(got == wrapped, "byte-mismatch in 1 MB paste");
}
#[test]
fn paste_payload_with_all_byte_values_round_trips() {
skip_if_unsupported!();
let session = EchoerSession::spawn(&[]);
let mut payload = Vec::with_capacity(256);
for b in 0u8..=255u8 {
payload.push(b);
}
let wrapped = wrap_paste(&payload);
session.write_stdin(&wrapped);
let got = session.drain_for(Duration::from_secs(10), Some(wrapped.len()));
assert_eq!(got.len(), wrapped.len(), "byte count mismatch");
assert!(got == wrapped, "byte-mismatch in all-256-values paste");
}
#[test]
fn paste_with_unicode_path_preserves_bytes() {
skip_if_unsupported!();
let session = EchoerSession::spawn(&[]);
let payload = b"[image:/tmp/clud paste 7f3a91 (drag drop) \xe2\x9c\x93.png]";
let wrapped = wrap_paste(payload);
session.write_stdin(&wrapped);
session.assert_received_exact(&wrapped, RECEIVE_TIMEOUT);
}
#[test]
fn embedded_paste_markers_are_not_consumed_by_substrate() {
skip_if_unsupported!();
let session = EchoerSession::spawn(&[]);
let mut inner = Vec::new();
inner.extend_from_slice(PASTE_OPEN);
inner.extend_from_slice(b"nested");
inner.extend_from_slice(PASTE_CLOSE);
let wrapped = wrap_paste(&inner);
session.write_stdin(&wrapped);
session.assert_received_exact(&wrapped, RECEIVE_TIMEOUT);
}
#[test]
fn two_back_to_back_pastes_arrive_in_order() {
skip_if_unsupported!();
let session = EchoerSession::spawn(&[]);
let mut both = Vec::new();
both.extend_from_slice(&wrap_paste(b"one"));
both.extend_from_slice(&wrap_paste(b"two"));
session.write_stdin(&both);
session.assert_received_exact(&both, RECEIVE_TIMEOUT);
}
#[test]
fn paste_interleaves_with_concurrent_typed_input() {
skip_if_unsupported!();
let session = Arc::new(EchoerSession::spawn(&[]));
let typist = {
let s = Arc::clone(&session);
std::thread::spawn(move || {
for _ in 0..100 {
s.write_stdin(b"q");
std::thread::sleep(Duration::from_millis(1));
}
})
};
std::thread::sleep(Duration::from_millis(50));
let paste = wrap_paste(b"[image:/tmp/x.png]");
session.write_stdin(&paste);
typist.join().expect("typist join");
let expected_total = 100 + paste.len();
let got = session.drain_for(RECEIVE_TIMEOUT, Some(expected_total));
assert_eq!(
got.len(),
expected_total,
"expected {} bytes total, got {}",
expected_total,
got.len()
);
let q_count = got.iter().filter(|b| **b == b'q').count();
assert_eq!(q_count, 100, "expected 100 'q' bytes, got {q_count}");
let open_pos = got
.windows(PASTE_OPEN.len())
.position(|w| w == PASTE_OPEN)
.expect("paste open marker missing");
assert!(got.len() >= open_pos + paste.len(), "paste tail truncated");
assert_eq!(
&got[open_pos..open_pos + paste.len()],
&paste[..],
"paste payload not contiguous in output"
);
}
#[test]
fn child_paste_enable_sequence_reaches_host() {
skip_if_unsupported!();
#[cfg(windows)]
{
eprintln!(
"[SKIP] child_paste_enable_sequence_reaches_host — Windows Server 2025 \
ConPTY renderer swallows the testbin's startup writes when an extra \
`\\x1b[?2004h` follows the handshake byte. Substrate-byte transit is \
still covered by every other test in this file. See #452."
);
}
#[cfg(not(windows))]
{
let session = EchoerSession::spawn(&["--advertise-paste"]);
let observed = session.drain_until_contains(b"\x1b[?2004h", Duration::from_secs(2));
assert!(
observed
.windows(b"\x1b[?2004h".len())
.any(|w| w == b"\x1b[?2004h"),
"expected bracketed-paste enable sequence in output, got {} bytes: {:?}",
observed.len(),
observed
);
}
}
#[test]
fn four_megabyte_paste_survives_slow_consumer() {
skip_if_unsupported!();
let bin = common::mitm_stdin::testbin_path("testbin-slow-stdin-reader");
let argv = vec![
bin.to_string_lossy().into_owned(),
"--sleep-ms".into(),
"2".into(),
"--buf-size".into(),
"4096".into(),
];
let process = Arc::new(
running_process::pty::NativePtyProcess::new(argv, None, None, 24, 80, None)
.expect("construct slow reader"),
);
process.start_impl().expect("start slow reader");
let handshake_deadline = Instant::now() + Duration::from_secs(40);
let mut saw_handshake = false;
while !saw_handshake && Instant::now() < handshake_deadline {
if let Ok(Some(chunk)) = process.read_chunk_impl(Some(0.1)) {
if chunk.contains(&common::mitm_stdin::STARTUP_HANDSHAKE_BYTE) {
saw_handshake = true;
}
}
}
assert!(
saw_handshake,
"testbin-slow-stdin-reader never emitted handshake byte"
);
let payload = vec![0xCDu8; 4 * 1024 * 1024];
let wrapped = Arc::new(wrap_paste(&payload));
let target_len = wrapped.len();
let writer = {
let process = Arc::clone(&process);
let wrapped = Arc::clone(&wrapped);
std::thread::spawn(move || {
process
.write_impl(&wrapped, false)
.expect("write large paste");
})
};
let deadline = Instant::now() + Duration::from_secs(100);
let mut got = Vec::with_capacity(target_len);
while got.len() < target_len && Instant::now() < deadline {
let chunk = process
.read_chunk_impl(Some(1.0))
.expect("read_chunk_impl")
.unwrap_or_default();
got.extend_from_slice(&chunk);
}
writer.join().expect("writer join");
let _ = process.kill_impl();
assert_eq!(
got.len(),
target_len,
"slow reader truncated paste: expected {} bytes, got {} (after 100s)",
target_len,
got.len()
);
assert!(*got == **wrapped, "byte-mismatch under backpressure");
}
#[test]
fn eof_mid_paste_drains_partial_without_hang() {
skip_if_unsupported!();
let session = EchoerSession::spawn(&[]);
let half_payload = vec![0xEEu8; 4096];
let mut partial = Vec::new();
partial.extend_from_slice(PASTE_OPEN);
partial.extend_from_slice(&half_payload);
session.write_stdin(&partial);
let got = session.drain_for(Duration::from_secs(2), Some(partial.len()));
assert!(
got.len() >= partial.len(),
"expected at least {} bytes echoed, got {}",
partial.len(),
got.len()
);
assert_eq!(&got[..partial.len()], &partial[..], "echo prefix mismatch");
}
#[test]
fn utf8_emoji_split_across_writes_arrives_whole() {
skip_if_unsupported!();
let session = EchoerSession::spawn(&[]);
session.write_stdin(b"\xf0\x9f");
session.write_stdin(b"\x98\x80");
session.assert_received_exact(b"\xf0\x9f\x98\x80", RECEIVE_TIMEOUT);
}
#[test]
fn resize_during_large_paste_does_not_corrupt_payload() {
skip_if_unsupported!();
let session = EchoerSession::spawn(&[]);
let payload = vec![0x55u8; 1_048_576];
let wrapped = wrap_paste(&payload);
let resize_thread = {
let proc = session.process();
let handles = Arc::clone(&proc.handles);
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(30));
let guard = handles.lock().expect("handles mutex poisoned");
if let Some(h) = guard.as_ref() {
let _ = h.master.resize(PtySize {
rows: 40,
cols: 132,
pixel_width: 0,
pixel_height: 0,
});
}
})
};
session.write_stdin(&wrapped);
resize_thread.join().expect("resize join");
let got = session.drain_for(Duration::from_secs(30), Some(wrapped.len()));
assert_eq!(
got.len(),
wrapped.len(),
"resize mid-paste truncated: expected {}, got {}",
wrapped.len(),
got.len()
);
assert!(got == wrapped, "byte-mismatch after resize-during-paste");
}
#[test]
fn concurrent_host_writers_do_not_tear_single_payloads() {
skip_if_unsupported!();
#[cfg(windows)]
{
eprintln!(
"[SKIP] concurrent_host_writers_do_not_tear_single_payloads — Windows \
Server 2025 ConPTY substrate does not deliver this test's startup \
handshake even at 40s + nextest serialization. The host-writer \
atomicity property is platform-neutral (it sits inside \
NativePtyProcess::write_impl) and is verified on Linux/macOS CI. \
See #452."
);
}
#[cfg(not(windows))]
{
let session = Arc::new(EchoerSession::spawn(&[]));
let payload_a: Vec<u8> = (0u8..64).map(|i| b'A' + (i % 26)).collect();
let payload_b: Vec<u8> = (0u8..64).map(|i| b'a' + (i % 26)).collect();
let a = {
let s = Arc::clone(&session);
let p = payload_a.clone();
std::thread::spawn(move || s.write_stdin(&p))
};
let b = {
let s = Arc::clone(&session);
let p = payload_b.clone();
std::thread::spawn(move || s.write_stdin(&p))
};
a.join().expect("a join");
b.join().expect("b join");
let got = session.drain_for(RECEIVE_TIMEOUT, Some(128));
assert_eq!(
got.len(),
128,
"expected 128 bytes total, got {}",
got.len()
);
let pos_a = got
.windows(payload_a.len())
.position(|w| w == payload_a.as_slice());
let pos_b = got
.windows(payload_b.len())
.position(|w| w == payload_b.as_slice());
assert!(
pos_a.is_some(),
"payload A torn or missing from output: {got:?}"
);
assert!(
pos_b.is_some(),
"payload B torn or missing from output: {got:?}"
);
}
}