use super::super::{LiveTail, overlay};
use super::{
AGENT, CountingRepaint, Harness, append, painted, response, rigged, text_delta, thinking_delta,
};
use crate::git_tree::{Delta, Stream};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn a_half_written_line_waits_for_its_newline() {
let h = Harness::new();
let (mut rig, mut follower) = rigged(&h);
let file = response(&h, AGENT, 1);
let whole = text_delta("atomic");
let (head, tail) = whole.split_at(whole.len() / 2);
append(&file, head);
assert!(!follower.pass(), "nothing whole arrived");
rig.refresh();
assert_eq!(painted(&rig, &h.ws, AGENT).text, None);
append(&file, tail);
assert!(follower.pass());
rig.refresh();
assert_eq!(painted(&rig, &h.ws, AGENT).text.as_deref(), Some("atomic"));
}
#[test]
fn an_idle_stream_publishes_nothing_so_a_quiet_call_costs_no_repaints() {
let h = Harness::new();
let (_rig, mut follower) = rigged(&h);
let file = response(&h, AGENT, 1);
append(&file, &text_delta("said"));
assert!(follower.pass(), "the first bytes are news");
assert!(!follower.pass(), "an unchanged file is not");
assert!(!follower.pass());
append(&file, "{\"type\":\"message_start\"}\n");
assert!(
!follower.pass(),
"an event outside the delta seam wakes nobody"
);
}
#[test]
fn focus_moving_drops_the_tail_and_opens_the_new_conversation() {
let h = Harness::new();
h.build_more("c-2", "second");
let (mut rig, mut follower) = rigged(&h);
append(&response(&h, AGENT, 1), &text_delta("first conversation"));
follower.pass();
rig.refresh();
assert_eq!(
painted(&rig, &h.ws, AGENT).text.as_deref(),
Some("first conversation")
);
append(&response(&h, "c-2", 1), &text_delta("second conversation"));
rig.focus_agent(&h.ws, "c-2");
rig.refresh();
assert!(follower.pass());
rig.refresh();
assert_eq!(
painted(&rig, &h.ws, "c-2").text.as_deref(),
Some("second conversation")
);
assert_eq!(
painted(&rig, &h.ws, AGENT).text,
None,
"the conversation just left is back to what the derivation says"
);
}
#[test]
fn a_new_step_starts_the_accumulator_over() {
let h = Harness::new();
let (mut rig, mut follower) = rigged(&h);
append(&response(&h, AGENT, 1), &text_delta("step one's answer"));
follower.pass();
rig.refresh();
assert_eq!(
painted(&rig, &h.ws, AGENT).text.as_deref(),
Some("step one's answer")
);
append(&response(&h, AGENT, 2), &text_delta("step two's"));
assert!(follower.pass());
rig.refresh();
assert_eq!(
painted(&rig, &h.ws, AGENT).text.as_deref(),
Some("step two's")
);
}
#[test]
fn a_truncated_file_is_re_read_from_the_start() {
let h = Harness::new();
let (mut rig, mut follower) = rigged(&h);
let file = response(&h, AGENT, 1);
append(&file, &text_delta("aaaaaaaaaaaaaaaaaaaa"));
follower.pass();
std::fs::write(&file, text_delta("b")).expect("truncate");
assert!(follower.pass());
rig.refresh();
assert!(
painted(&rig, &h.ws, AGENT)
.text
.as_deref()
.is_some_and(|t| t.ends_with('b')),
"the bytes that are there now are what folded"
);
}
#[test]
fn nothing_focused_and_nothing_opened_are_the_same_empty_answer() {
let h = Harness::new();
let (_clock, mut rig) = h.model();
let mut follower = rig.follower();
rig.refresh();
assert!(!follower.pass());
rig.focus_agent(&h.ws, AGENT);
rig.refresh();
assert!(!follower.pass());
append(&response(&h, AGENT, 1), &text_delta("hi"));
assert!(follower.pass());
rig.focus_agent(&h.ws, "never-ran");
rig.refresh();
assert!(follower.pass(), "the tail retiring is news too");
assert!(!follower.pass(), "and then there is nothing to say");
rig.focus_workspace(&h.ws);
rig.refresh();
assert!(!follower.pass());
}
#[test]
fn a_tail_for_an_agent_the_snapshot_does_not_carry_writes_nothing() {
let h = Harness::new();
let (_clock, rig) = h.model();
let mut snap = (**rig.model.derivation()).clone();
overlay(
&mut snap,
&LiveTail {
ws: "/nowhere".into(),
agent: AGENT.to_owned(),
stream: Stream::default(),
},
);
overlay(
&mut snap,
&LiveTail {
ws: h.ws.clone(),
agent: "no-such-agent".to_owned(),
stream: Stream::default(),
},
);
assert_eq!(
snap.trees.get(&h.ws).map(|t| t.agents.len()),
Some(1),
"no row minted, none removed"
);
}
#[test]
fn the_spawned_follower_runs_the_real_thread_and_wakes_the_face() {
let h = Harness::new();
let (mut rig, follower) = rigged(&h);
let file = response(&h, AGENT, 1);
let count = Arc::new(AtomicUsize::new(0));
let thread = follower.spawn(CountingRepaint(Arc::clone(&count)));
append(&file, &text_delta("landed"));
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while count.load(Ordering::Relaxed) == 0 && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert!(
count.load(Ordering::Relaxed) >= 1,
"the follower requested a repaint when characters landed"
);
drop(thread); rig.refresh();
assert_eq!(painted(&rig, &h.ws, AGENT).text.as_deref(), Some("landed"));
}
#[test]
fn absorbing_a_suffix_is_the_same_fold_as_reading_the_whole_file() {
let head = thinking_delta("mm");
let whole = format!("{head}{}{}", text_delta("ab"), text_delta("cd"));
let mut resumed = crate::git_tree::fold_stream(head.as_bytes());
resumed.absorb(crate::git_tree::fold_stream(
whole.get(head.len()..).expect("suffix").as_bytes(),
));
assert_eq!(resumed, crate::git_tree::fold_stream(whole.as_bytes()));
assert_eq!(resumed.text.as_deref(), Some("abcd"));
assert_eq!(resumed.thinking.as_deref(), Some("mm"));
assert_eq!(resumed.last_delta, Some(Delta::Text));
let mut held = resumed.clone();
held.absorb(Stream::default());
assert_eq!(held, resumed);
}