use crate::input::{UserInput, WireEvent};
use crate::predict::{DisplayPreference, PredictionEngine};
use crate::ssp::testkit::{LinkParams, SimHarness};
use crate::terminal::{ServerTerminal, TerminalScreen};
pub struct SessionResult {
pub converge_steps: usize,
pub sim_ms: u64,
pub client_text: String,
pub client_echo_ack: u64,
pub expected_frame: u64,
}
impl SessionResult {
pub fn assert_ok(&self) -> anyhow::Result<()> {
if !self.client_text.contains("hello koh") {
anyhow::bail!(
"client screen missing expected output; got:\n{}",
self.client_text
);
}
if self.client_echo_ack != self.expected_frame {
anyhow::bail!(
"echo_ack mismatch: client={} expected={}",
self.client_echo_ack,
self.expected_frame
);
}
Ok(())
}
}
pub fn run_session(loss: f64, seed: u64) -> SessionResult {
let params = LinkParams {
loss,
min_delay_ms: 10,
max_delay_ms: 60,
dup: 0.02,
};
let mut h = SimHarness::<UserInput, TerminalScreen>::new(params, seed, 1200);
let mut emu = ServerTerminal::new(24, 80, 0);
emu.process(b"$ ");
*h.b_mut() = emu.snapshot();
h.run_until(5_000, |h| {
h.a.remote_state().screen().contents().contains('$')
});
let cmd = b"echo hello koh\r";
h.a_mut().push_bytes(cmd);
h.run_until(20_000, |h| h.b.remote_state().len() >= cmd.len());
let frame = h.b.remote_num();
let arrival = h.now();
emu.register_input_frame(frame, arrival);
for w in h.b.get_remote_diff() {
if let WireEvent::Keys(bytes) = w {
for b in bytes {
if b == b'\r' {
emu.process(b"\r\nhello koh\r\n$ ");
} else {
emu.process(&[b]);
}
}
}
}
emu.set_echo_ack(arrival + 1_000);
*h.b_mut() = emu.snapshot();
let target = emu.snapshot();
let converge_steps = h.run_until(40_000, |h| *h.a.remote_state() == target);
SessionResult {
converge_steps,
sim_ms: h.now(),
client_text: h.a.remote_state().screen().contents(),
client_echo_ack: h.a.remote_state().echo_ack(),
expected_frame: frame,
}
}
pub fn run_predictor_reconciliation() -> anyhow::Result<()> {
let mut pe = PredictionEngine::new(DisplayPreference::Always);
pe.set_local_frame_sent(0);
let blank = TerminalScreen::default();
pe.new_user_byte(100, b'h', blank.screen());
if !pe.overlay(blank.screen()).is_empty() {
anyhow::bail!("predictor leaked a keystroke before the server confirmed it echoes");
}
let echoed = TerminalScreen::from_bytes(24, 80, b"h");
pe.set_local_frame_late_acked(1);
pe.cull(200, echoed.screen());
if !pe.overlay(echoed.screen()).is_empty() {
anyhow::bail!("confirmed prediction should be cleared");
}
Ok(())
}