2026-05-28 18:17:01 -06:00
|
|
|
use alloc::{vec, vec::Vec};
|
|
|
|
|
|
2026-06-01 09:54:37 -06:00
|
|
|
use unshell::protocol::{Leaf, Packet};
|
2026-05-28 18:17:01 -06:00
|
|
|
|
2026-06-01 09:54:37 -06:00
|
|
|
use crate::{
|
|
|
|
|
FakePtyLeaf, FakePtyState, OP_ABORT, OP_ERROR, OP_EXIT, OP_INPUT, OP_OUTPUT, OP_STDIN_EOF,
|
|
|
|
|
OP_TERMINATE, pty_open_packet,
|
2026-05-28 18:17:01 -06:00
|
|
|
};
|
|
|
|
|
|
2026-06-01 09:54:37 -06:00
|
|
|
use super::support::{
|
|
|
|
|
ENDPOINT_A, ENDPOINT_B, PROC_OTHER, assert_frame, assert_hook_present, assert_hook_removed,
|
|
|
|
|
assert_opened, drain_parent_pty_packets, endpoint_at, has_frame, open_pty_session,
|
|
|
|
|
pty_endpoints, send_downward_frame, transfer_packets,
|
|
|
|
|
};
|
2026-05-28 18:17:01 -06:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn open_pty_paves_hook_and_creates_session() {
|
|
|
|
|
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
|
|
|
|
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
|
|
|
|
|
|
|
|
|
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
|
|
|
|
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
|
|
|
|
|
|
|
|
|
assert_eq!(leaf.active_session_count(), 1);
|
|
|
|
|
assert_eq!(leaf.state().active_count, 1);
|
|
|
|
|
assert_eq!(leaf.state().total_opened, 1);
|
|
|
|
|
assert_hook_present(&endpoint_a, hook_id);
|
|
|
|
|
assert_hook_present(&endpoint_b, hook_id);
|
|
|
|
|
assert_eq!(packets.len(), 1);
|
2026-06-01 09:54:37 -06:00
|
|
|
assert_opened(&packets[0], hook_id);
|
2026-05-28 18:17:01 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn input_and_output_share_one_hook() {
|
|
|
|
|
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
|
|
|
|
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
|
|
|
|
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
|
|
|
|
drain_parent_pty_packets(&mut endpoint_a);
|
|
|
|
|
|
|
|
|
|
send_downward_frame(
|
|
|
|
|
&mut endpoint_a,
|
|
|
|
|
&mut endpoint_b,
|
|
|
|
|
hook_id,
|
|
|
|
|
OP_INPUT,
|
|
|
|
|
b"hello",
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
leaf.update(&mut endpoint_b);
|
|
|
|
|
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
|
|
|
|
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
|
|
|
|
|
|
|
|
|
assert_eq!(packets.len(), 1);
|
|
|
|
|
assert_frame(&packets[0], hook_id, OP_OUTPUT, false, b"hello");
|
|
|
|
|
assert_hook_present(&endpoint_a, hook_id);
|
|
|
|
|
assert_hook_present(&endpoint_b, hook_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn stdin_eof_keeps_hook_until_exit() {
|
|
|
|
|
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
|
|
|
|
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
|
|
|
|
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
|
|
|
|
drain_parent_pty_packets(&mut endpoint_a);
|
|
|
|
|
|
|
|
|
|
send_downward_frame(
|
|
|
|
|
&mut endpoint_a,
|
|
|
|
|
&mut endpoint_b,
|
|
|
|
|
hook_id,
|
|
|
|
|
OP_STDIN_EOF,
|
|
|
|
|
&[],
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
leaf.update(&mut endpoint_b);
|
|
|
|
|
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
|
|
|
|
|
|
|
|
|
assert_eq!(leaf.state().last_stdin_eof_hook, Some(hook_id));
|
|
|
|
|
assert!(drain_parent_pty_packets(&mut endpoint_a).is_empty());
|
|
|
|
|
assert_hook_present(&endpoint_a, hook_id);
|
|
|
|
|
assert_hook_present(&endpoint_b, hook_id);
|
|
|
|
|
|
|
|
|
|
send_downward_frame(
|
|
|
|
|
&mut endpoint_a,
|
|
|
|
|
&mut endpoint_b,
|
|
|
|
|
hook_id,
|
|
|
|
|
OP_TERMINATE,
|
|
|
|
|
&[],
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
leaf.update(&mut endpoint_b);
|
|
|
|
|
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
|
|
|
|
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
|
|
|
|
|
|
|
|
|
assert_eq!(packets.len(), 1);
|
|
|
|
|
assert_frame(&packets[0], hook_id, OP_EXIT, true, &[0]);
|
|
|
|
|
assert_eq!(leaf.active_session_count(), 0);
|
|
|
|
|
assert_hook_removed(&endpoint_a, hook_id);
|
|
|
|
|
assert_hook_removed(&endpoint_b, hook_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn exit_end_hook_cleans_route_and_session() {
|
|
|
|
|
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
|
|
|
|
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
|
|
|
|
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
|
|
|
|
drain_parent_pty_packets(&mut endpoint_a);
|
|
|
|
|
|
|
|
|
|
send_downward_frame(
|
|
|
|
|
&mut endpoint_a,
|
|
|
|
|
&mut endpoint_b,
|
|
|
|
|
hook_id,
|
|
|
|
|
OP_TERMINATE,
|
|
|
|
|
&[],
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
leaf.update(&mut endpoint_b);
|
|
|
|
|
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
|
|
|
|
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
|
|
|
|
|
|
|
|
|
assert_eq!(packets.len(), 1);
|
|
|
|
|
assert_frame(&packets[0], hook_id, OP_EXIT, true, &[0]);
|
|
|
|
|
assert_eq!(leaf.active_session_count(), 0);
|
|
|
|
|
assert_hook_removed(&endpoint_a, hook_id);
|
|
|
|
|
assert_hook_removed(&endpoint_b, hook_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn failed_final_exit_route_retries_without_losing_session() {
|
|
|
|
|
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
|
|
|
|
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
|
|
|
|
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
|
|
|
|
drain_parent_pty_packets(&mut endpoint_a);
|
|
|
|
|
|
|
|
|
|
send_downward_frame(
|
|
|
|
|
&mut endpoint_a,
|
|
|
|
|
&mut endpoint_b,
|
|
|
|
|
hook_id,
|
|
|
|
|
OP_TERMINATE,
|
|
|
|
|
&[],
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
endpoint_b.connections.remove(&(ENDPOINT_A, true));
|
|
|
|
|
leaf.update(&mut endpoint_b);
|
|
|
|
|
|
|
|
|
|
assert_eq!(leaf.active_session_count(), 1);
|
|
|
|
|
assert_eq!(leaf.pending_packet_count(), 1);
|
|
|
|
|
assert_hook_present(&endpoint_b, hook_id);
|
|
|
|
|
|
|
|
|
|
endpoint_b.connections.insert((ENDPOINT_A, true));
|
|
|
|
|
leaf.update(&mut endpoint_b);
|
|
|
|
|
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
|
|
|
|
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
|
|
|
|
|
|
|
|
|
assert_eq!(packets.len(), 1);
|
|
|
|
|
assert_frame(&packets[0], hook_id, OP_EXIT, true, &[0]);
|
|
|
|
|
assert_eq!(leaf.active_session_count(), 0);
|
|
|
|
|
assert_hook_removed(&endpoint_a, hook_id);
|
|
|
|
|
assert_hook_removed(&endpoint_b, hook_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn abort_downward_end_hook_closes_without_ack() {
|
|
|
|
|
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
|
|
|
|
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
|
|
|
|
let hook_id = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
|
|
|
|
drain_parent_pty_packets(&mut endpoint_a);
|
|
|
|
|
|
|
|
|
|
send_downward_frame(
|
|
|
|
|
&mut endpoint_a,
|
|
|
|
|
&mut endpoint_b,
|
|
|
|
|
hook_id,
|
|
|
|
|
OP_ABORT,
|
|
|
|
|
&[],
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
leaf.update(&mut endpoint_b);
|
|
|
|
|
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
|
|
|
|
|
|
|
|
|
assert_eq!(leaf.active_session_count(), 0);
|
|
|
|
|
assert!(drain_parent_pty_packets(&mut endpoint_a).is_empty());
|
|
|
|
|
assert_hook_removed(&endpoint_a, hook_id);
|
|
|
|
|
assert_hook_removed(&endpoint_b, hook_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn unknown_session_input_returns_error_end_hook() {
|
|
|
|
|
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
|
|
|
|
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
|
|
|
|
let hook_id = endpoint_a.get_hook_id();
|
|
|
|
|
|
|
|
|
|
send_downward_frame(
|
|
|
|
|
&mut endpoint_a,
|
|
|
|
|
&mut endpoint_b,
|
|
|
|
|
hook_id,
|
|
|
|
|
OP_INPUT,
|
|
|
|
|
b"orphan",
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
leaf.update(&mut endpoint_b);
|
|
|
|
|
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
|
|
|
|
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
|
|
|
|
|
|
|
|
|
assert_eq!(packets.len(), 1);
|
|
|
|
|
assert_frame(&packets[0], hook_id, OP_ERROR, true, b"unknown-session");
|
|
|
|
|
assert_eq!(leaf.active_session_count(), 0);
|
|
|
|
|
assert_hook_removed(&endpoint_a, hook_id);
|
|
|
|
|
assert_hook_removed(&endpoint_b, hook_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn two_pty_sessions_interleave_without_crossing_hooks() {
|
|
|
|
|
let (mut endpoint_a, mut endpoint_b) = pty_endpoints();
|
|
|
|
|
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
|
|
|
|
|
|
|
|
|
let first_hook = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
|
|
|
|
let second_hook = open_pty_session(&mut endpoint_a, &mut endpoint_b, &mut leaf);
|
|
|
|
|
drain_parent_pty_packets(&mut endpoint_a);
|
|
|
|
|
|
|
|
|
|
send_downward_frame(
|
|
|
|
|
&mut endpoint_a,
|
|
|
|
|
&mut endpoint_b,
|
|
|
|
|
second_hook,
|
|
|
|
|
OP_INPUT,
|
|
|
|
|
b"second",
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
send_downward_frame(
|
|
|
|
|
&mut endpoint_a,
|
|
|
|
|
&mut endpoint_b,
|
|
|
|
|
first_hook,
|
|
|
|
|
OP_INPUT,
|
|
|
|
|
b"first",
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
leaf.update(&mut endpoint_b);
|
|
|
|
|
transfer_packets(&mut endpoint_b, &mut endpoint_a, ENDPOINT_A, ENDPOINT_B);
|
|
|
|
|
let packets = drain_parent_pty_packets(&mut endpoint_a);
|
|
|
|
|
|
|
|
|
|
assert_eq!(leaf.active_session_count(), 2);
|
|
|
|
|
assert_eq!(packets.len(), 2);
|
|
|
|
|
assert!(has_frame(&packets, first_hook, OP_OUTPUT, b"first"));
|
|
|
|
|
assert!(has_frame(&packets, second_hook, OP_OUTPUT, b"second"));
|
|
|
|
|
assert_hook_present(&endpoint_a, first_hook);
|
|
|
|
|
assert_hook_present(&endpoint_a, second_hook);
|
|
|
|
|
assert_hook_present(&endpoint_b, first_hook);
|
|
|
|
|
assert_hook_present(&endpoint_b, second_hook);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn pty_leaf_does_not_consume_other_leaf_packets() {
|
|
|
|
|
let mut endpoint = endpoint_at(ENDPOINT_B, vec![ENDPOINT_A, ENDPOINT_B]);
|
|
|
|
|
let mut leaf = FakePtyLeaf::new(FakePtyState::new());
|
|
|
|
|
endpoint.connections.insert((ENDPOINT_A, true));
|
|
|
|
|
|
|
|
|
|
endpoint
|
|
|
|
|
.add_inbound_from(
|
|
|
|
|
ENDPOINT_A,
|
|
|
|
|
pty_open_packet(vec![ENDPOINT_A, ENDPOINT_B], 7, &[ENDPOINT_A]),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
endpoint
|
|
|
|
|
.add_inbound_from(
|
|
|
|
|
ENDPOINT_A,
|
|
|
|
|
Packet {
|
|
|
|
|
hook_id: 8,
|
|
|
|
|
end_hook: false,
|
|
|
|
|
path: vec![ENDPOINT_A, ENDPOINT_B],
|
|
|
|
|
procedure_id: PROC_OTHER,
|
|
|
|
|
data: b"leave-me".to_vec(),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
leaf.update(&mut endpoint);
|
|
|
|
|
|
|
|
|
|
let mut other_packets = Vec::new();
|
|
|
|
|
endpoint.take_inbound_matching(
|
|
|
|
|
ENDPOINT_B,
|
|
|
|
|
|packet| packet.procedure_id == PROC_OTHER,
|
|
|
|
|
|packet| other_packets.push(packet),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert_eq!(leaf.active_session_count(), 1);
|
|
|
|
|
assert_eq!(other_packets.len(), 1);
|
|
|
|
|
assert_eq!(other_packets[0].procedure_id, PROC_OTHER);
|
|
|
|
|
assert_eq!(other_packets[0].data, b"leave-me".to_vec());
|
|
|
|
|
}
|