use crate::input::{UserInput, WireEvent};
use crate::predict::{DisplayPreference, PredictionEngine};
use crate::ssp::testkit::{GridState, 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().events().len() >= cmd.len());
let frame = h.b.remote_num();
let arrival = h.now();
let mut echo = crate::server::EchoAck::default();
echo.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]);
}
}
}
}
echo.set_echo_ack(arrival + 1_000);
let mut snap = emu.snapshot();
snap.set_echo_ack(echo.echo_ack());
*h.b_mut() = snap.clone();
let target = snap;
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(())
}
pub struct GenericSessionResult {
pub converge_steps: usize,
pub sim_ms: u64,
pub client_view: GridState,
pub server_view_of_input: usize,
pub expected_input: usize,
}
impl GenericSessionResult {
pub fn assert_ok(&self, expected: &GridState) -> anyhow::Result<()> {
if self.client_view != *expected {
anyhow::bail!(
"client replica diverged from the server state: {} cells vs {}",
self.client_view.cells.len(),
expected.cells.len()
);
}
if self.server_view_of_input != self.expected_input {
anyhow::bail!(
"server saw {} input events, expected {}",
self.server_view_of_input,
self.expected_input
);
}
Ok(())
}
}
pub fn run_generic_session(loss: f64, seed: u64, rounds: u32) -> (GenericSessionResult, GridState) {
let params = LinkParams {
loss,
min_delay_ms: 10,
max_delay_ms: 60,
dup: 0.02,
};
let mut h = SimHarness::<UserInput, GridState>::new(params, seed, 1200);
let mut rng = crate::ssp::testkit::Rng::new(seed ^ 0xA5A5);
let mut typed = 0usize;
for round in 0..rounds {
let k = (rng.next_u64() % 12) as u32;
let len = rng.range(1, 2500) as usize;
h.b_mut().cells.insert(k, vec![round as u8; len]);
if (rng.next_u64()).is_multiple_of(3) {
h.b_mut().cells.remove(&((rng.next_u64() % 12) as u32));
}
h.a_mut().push_bytes(b"k");
typed += 1;
h.run_steps(5);
}
let target = h.b.current().clone();
let converge_steps = h.run_until(60_000, |h| {
*h.a.remote_state() == target && h.b.remote_state().events().len() >= typed
});
let result = GenericSessionResult {
converge_steps,
sim_ms: h.now(),
client_view: h.a.remote_state().clone(),
server_view_of_input: h.b.remote_state().events().len(),
expected_input: typed,
};
(result, target)
}