use super::*;
#[test]
fn a_context_count_moves_when_its_figure_would() {
let f = |n: u64| ctx_figure(Some(n));
assert_eq!(f(412_000), f(412_999), "412k either way");
assert_ne!(f(412_999), f(413_000));
assert_eq!(f(1_200_000), f(1_299_999), "1.2M either way");
assert_ne!(f(1_299_999), f(1_300_000));
assert_ne!(f(999_999), f(1_000_000), "999k is not 1.0M");
assert_ne!(f(10_000), f(1_000_000), "10k is not 1.0M");
assert_eq!(ctx_figure(None), None);
}
#[test]
fn a_folder_is_asked_about_its_tree_only_when_something_could_have_changed_it() {
let t0 = Instant::now();
assert!(status_due(t0, None, None, 1), "never asked: asked");
let asked = Asked {
at: t0,
cost: Duration::from_millis(20),
watchers: 1,
};
let later = t0 + GIT_FLOOR * 2;
assert!(
!status_due(later, Some(asked), Some(t0 - Duration::from_secs(1)), 1),
"a prompt that printed nothing since is not asked, however long ago"
);
assert!(
!status_due(later, Some(asked), None, 1),
"nor one that never printed"
);
assert!(
status_due(later, Some(asked), Some(t0 + Duration::from_secs(1)), 1),
"a pane printed since: asked"
);
assert!(
!status_due(
t0 + GIT_FLOOR / 2,
Some(asked),
Some(t0 + Duration::from_secs(1)),
1
),
"but not before the floor"
);
assert!(
status_due(later, Some(asked), None, 2),
"a page began watching: asked, printed or not"
);
assert!(
!status_due(later, Some(asked), None, 0),
"one stopped watching: nothing new to say"
);
assert!(
status_due(t0 + GIT_QUIET, Some(asked), None, 1),
"a watched folder is asked after a quiet minute: a commit from outside shows"
);
assert!(
!status_due(t0 + GIT_QUIET * 5, Some(asked), None, 0),
"an unwatched one is not"
);
let slow = Asked {
cost: Duration::from_secs(3),
..asked
};
assert!(
!status_due(
t0 + Duration::from_secs(29),
Some(slow),
Some(t0 + Duration::from_secs(1)),
1
),
"a slow repository backs off past the floor"
);
assert!(status_due(
t0 + Duration::from_secs(31),
Some(slow),
Some(t0 + Duration::from_secs(1)),
1
));
let glacial = Asked {
cost: Duration::from_secs(30),
..asked
};
assert!(
status_due(
t0 + GIT_AT_MOST + Duration::from_secs(1),
Some(glacial),
Some(t0 + Duration::from_secs(1)),
1
),
"and never further than the most"
);
}
#[test]
fn kept_text_is_cut_from_the_front_to_the_cap() {
let old: Vec<String> = (0..10).map(|i| format!("old {i}")).collect();
let big = "z".repeat(1024);
let now: Vec<String> = (0..3000).map(|_| big.clone()).collect();
let kept = keep_text(&old, now);
let bytes: usize = kept.iter().map(|l| l.len() + 1).sum();
assert!(bytes <= screen::SCROLLBACK_BYTES);
assert_eq!(kept.last().unwrap(), &big, "the newest line is kept");
assert!(
!kept.iter().any(|l| l.starts_with("old")),
"the oldest go first"
);
let kept = keep_text(&old, vec!["new".into()]);
assert_eq!(kept.len(), 11);
assert_eq!(kept[0], "old 0");
assert_eq!(kept[10], "new");
}
#[test]
fn a_restart_waits_for_agents_and_recent_output_but_not_forever() {
let s = |running: bool, agent: &'static str| Status {
running,
agent,
..Status::default()
};
let long_ago = BUSY_OUTPUT + Duration::from_secs(1);
let just_now = Duration::from_secs(1);
let a_while = Duration::from_secs(60);
let silent = WORKING_SILENT + Duration::from_secs(1);
let for_ever = PRINTING_AT_MOST + Duration::from_secs(1);
assert!(is_busy(&s(true, "working"), long_ago, a_while));
assert!(is_busy(&s(true, ""), just_now, a_while));
assert!(is_busy(&s(true, "done"), just_now, a_while));
assert!(!is_busy(&s(true, ""), long_ago, a_while));
assert!(!is_busy(&s(true, "done"), long_ago, a_while));
assert!(is_busy(&s(true, "needs_you"), long_ago, a_while));
assert!(is_busy(&s(true, "needs_you"), silent, for_ever));
assert!(!is_busy(&s(true, "working"), silent, a_while));
assert!(is_busy(&s(true, ""), just_now, PRINTING_AT_MOST - a_while));
assert!(!is_busy(&s(true, ""), just_now, for_ever));
assert!(!is_busy(&s(true, "done"), just_now, for_ever));
assert!(!is_busy(&s(false, "working"), just_now, a_while));
assert!(!is_busy(&s(false, "needs_you"), just_now, a_while));
assert!(!is_busy(&s(false, ""), just_now, a_while));
}
#[test]
fn a_mark_waits_for_a_look_then_lapses_into_an_offer() {
let t0 = Instant::now();
let m = |until: Option<Instant>| Marks {
resume: ["r".to_string()].into(),
offer: ["o".to_string()].into(),
resume_until: until,
cap: t0 + MARKS_AT_MOST,
};
let later = t0 + Duration::from_secs(10 * 60);
assert!(m(None).resume("r", later));
assert!(!m(None).offer("r", later));
assert!(m(None).offer("o", later));
let armed = m(Some(later + RESUME_FOR));
assert!(armed.resume("r", later + Duration::from_secs(60)));
assert!(!armed.resume("r", later + RESUME_FOR));
assert!(armed.offer("r", later + RESUME_FOR));
assert!(!armed.resume("o", later), "an offer is never a resume");
let day = t0 + MARKS_AT_MOST;
assert!(!m(None).resume("r", day));
assert!(!armed.offer("r", day));
assert!(!armed.offer("o", day));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_resume_mark_rides_the_status_until_the_pane_starts_or_it_lapses() {
let dir = crate::store::tempdir::Dir::new("snyvi-pane-mark");
let (events, _) = broadcast::channel(16);
let panes = Panes::new(&dir.path, events);
let early = "0000000000000000000000000000000a";
let late = "0000000000000000000000000000000b";
let never = "0000000000000000000000000000000c";
let woken = panes.get(early);
assert!(!woken.attach().0[0].contains("\"resume\":true"));
panes.mark_resume(vec![early.into(), late.into()]);
assert!(panes.status(early).resume);
assert!(panes.get(late).attach().0[0].contains("\"resume\":true"));
assert!(panes.status(late).resume);
assert!(!panes.status(never).resume);
assert!(panes.busy().is_empty(), "a stopped pane is never busy");
#[cfg(unix)]
{
let cwd = dir.path.to_string_lossy().to_string();
let s = Start {
cwd: &cwd,
root: &cwd,
cmd: "sleep 30",
desk: "d",
slot: 1,
cols: 80,
rows: 10,
accent: "",
offer: false,
env: &[],
};
let status = panes.get(late).start(s, &panes).unwrap();
assert!(!status.resume);
assert!(!panes.status(late).resume);
assert_eq!(panes.busy(), vec![late.to_string()]);
panes.get(late).stop();
}
assert!(panes.status(early).resume, "the other mark is untouched");
assert!(panes.unspent().0.contains(&early.to_string()));
#[cfg(unix)]
assert!(
!panes.unspent().0.contains(&late.to_string()),
"spent by its start"
);
panes.arm_marks();
panes.marks.lock().unwrap().resume_until = Some(Instant::now());
panes.refresh_marks();
assert!(!panes.status(early).resume);
assert!(panes.status(early).offer);
assert!(!panes.marked(early), "the page's own resume is refused now");
assert!(panes.unspent().0.is_empty());
assert!(
panes.unspent().1.contains(&early.to_string()),
"and written back as an offer"
);
assert!(!panes.get(never).attach().0[0].contains("\"resume\":true"));
}
#[test]
fn only_a_pane_id_is_a_file_name() {
assert!(valid_id("0123456789abcdef0123456789abcdef"));
assert!(!valid_id("../../etc/passwd"));
assert!(!valid_id("0123456789abcdef0123456789abcde/"));
assert!(!valid_id(""));
}
#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_pane_runs_a_process_and_its_output_arrives_as_frames() {
let dir = crate::store::tempdir::Dir::new("snyvi-pane");
let (events, _) = broadcast::channel(16);
let panes = Panes::new(&dir.path, events);
let id = "00112233445566778899aabbccddeeff";
let live = panes.get(id);
let (first, mut rx) = live.attach();
assert!(first[0].contains("\"running\":false"));
let cwd = dir.path.to_string_lossy().to_string();
live.start(
Start {
cwd: &cwd,
root: &cwd,
cmd: "printf 'pane=%s key=%s\\n' \"$SNYVI_SESSION\" \"$SNYVI_T\"; pwd; exit 3",
desk: "d",
slot: 1,
cols: 400,
rows: 10,
accent: "",
offer: false,
env: &[("SNYVI_T".to_string(), "x".to_string())],
},
&panes,
)
.unwrap();
let mut seen = String::new();
let mut exit = None;
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
while exit.is_none() && tokio::time::Instant::now() < deadline {
let Ok(Ok(msg)) = tokio::time::timeout(Duration::from_secs(10), rx.recv()).await else {
break;
};
let v: serde_json::Value = serde_json::from_str(&msg).unwrap();
if v["t"] == "frame" {
seen.push_str(&msg);
}
if v["t"] == "status" && v["s"]["running"] == false {
exit = v["s"]["exit"].as_i64();
}
}
while let Ok(Ok(msg)) = tokio::time::timeout(Duration::from_millis(300), rx.recv()).await {
seen.push_str(&msg);
}
assert!(seen.contains(&format!("pane={id}")), "{seen}");
assert!(seen.contains(&format!("pane={id} key=x")), "{seen}");
assert!(seen.contains(cwd.split('/').next_back().unwrap()), "{seen}");
assert_eq!(exit, Some(3));
let text = panes.read_text(id).join("\n");
assert!(text.contains(&format!("pane={id}")), "{text}");
}
#[tokio::test]
async fn what_a_pane_writes_down_in_pieces_reads_back_whole() {
let dir = crate::store::tempdir::Dir::new("snyvi-pane-persist");
let (events, _) = broadcast::channel(16);
let panes = Panes::new(&dir.path, events);
let id = "0f0e0d0c0b0a09080706050403020101";
let live = panes.get(id);
let feed = |text: String| {
let mut i = live.inner.lock().unwrap();
let Inner {
screen,
parser,
unsaved,
unsaved_lines,
..
} = &mut *i;
screen.feed(parser, text.as_bytes());
*unsaved = true;
*unsaved_lines = true;
};
let whole = || {
let i = live.inner.lock().unwrap();
keep_text(&i.old, i.screen.text())
};
feed((0..10_000).map(|k| format!("line {k}\r\n")).collect());
let want = whole();
assert!(want.len() > 9_000, "the scrollback holds the lines");
panes.persist_all(false);
assert_eq!(
panes.read_text(id),
want,
"the first write is the whole text"
);
assert!(dir.path.join("panes").join(format!("{id}.scr")).exists());
feed(
(10_000..10_500)
.map(|k| format!("line {k}\r\n"))
.collect::<String>()
+ "$ ",
);
let want = whole();
let text = {
let mut i = live.inner.lock().unwrap();
let t0 = Instant::now();
let text = i.take_text();
let held = t0.elapsed();
assert!(
held < Duration::from_millis(10),
"the lock was held {held:?}"
);
text
};
match &text {
Text::More { lines, screen, .. } => {
assert_eq!(lines.len(), 500, "the lines that left the screen since");
assert_eq!(
screen.last().map(String::as_str),
Some("$"),
"the screen, trailing blanks trimmed"
);
}
Text::Whole { .. } => panic!("the second write is the lines since, not the whole"),
}
panes.write(id, text);
assert_eq!(
panes.read_text(id),
want,
"in pieces reads back as the whole"
);
feed("\x1b[2K\r$ ls".to_string());
let want = whole();
let text = live.inner.lock().unwrap().take_text();
assert!(matches!(&text, Text::More { lines, .. } if lines.is_empty()));
panes.write(id, text);
assert_eq!(panes.read_text(id), want);
let big = "z".repeat(1000);
feed((0..2_500).map(|_| format!("{big}\r\n")).collect());
let want = whole();
assert!(bytes(want.iter().map(String::len)) <= screen::SCROLLBACK_BYTES);
let text = live.inner.lock().unwrap().take_text();
assert!(
matches!(text, Text::Whole { .. }),
"over the cap: whole again"
);
panes.write(id, text);
assert_eq!(panes.read_text(id), want);
feed("one more\r\n".to_string());
let want = whole();
let text = live.inner.lock().unwrap().take_text();
assert!(matches!(&text, Text::More { lines, .. } if lines.len() == 1));
panes.write(id, text);
assert_eq!(panes.read_text(id), want);
feed("\x1b[3J\x1b[H\x1b[2Jafter\r\n".to_string());
let want = whole();
assert!(!want.iter().any(|l| l.contains("line 10")), "cleared");
let text = live.inner.lock().unwrap().take_text();
assert!(matches!(text, Text::Whole { .. }), "after a clear: whole");
panes.write(id, text);
assert_eq!(panes.read_text(id), want);
assert!(!panes.read_text(id).iter().any(|l| l.contains("zzz")));
}
#[cfg(windows)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_verbatim_folder_starts_the_panel_in_that_folder() {
let dir = crate::store::tempdir::Dir::new("snyvi-pane-unc");
let (events, _) = broadcast::channel(16);
let panes = Panes::new(&dir.path, events);
let id = "00112233445566778899aabbccddeef0";
let live = panes.get(id);
let (_, mut rx) = live.attach();
let cwd = std::fs::canonicalize(&dir.path)
.unwrap()
.to_string_lossy()
.to_string();
assert!(cwd.starts_with(r"\\?\"), "the case this is about: {cwd}");
live.start(
Start {
cwd: &cwd,
root: &cwd,
cmd: "cd",
desk: "d",
slot: 1,
cols: 400,
rows: 10,
accent: "",
offer: false,
env: &[],
},
&panes,
)
.unwrap();
let mut seen = String::new();
let mut done = false;
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
while !done && tokio::time::Instant::now() < deadline {
let Ok(Ok(msg)) = tokio::time::timeout(Duration::from_secs(10), rx.recv()).await else {
break;
};
let v: serde_json::Value = serde_json::from_str(&msg).unwrap();
if v["t"] == "frame" {
seen.push_str(&msg);
}
done = v["t"] == "status" && v["s"]["running"] == false;
}
while let Ok(Ok(msg)) = tokio::time::timeout(Duration::from_millis(300), rx.recv()).await {
seen.push_str(&msg);
}
assert!(!seen.contains("UNC paths are not supported"), "{seen}");
let name = dir.path.file_name().unwrap().to_string_lossy().to_string();
assert!(seen.contains(&name), "the panel is not in {name}: {seen}");
}
#[test]
fn a_panel_starts_outside_the_session_that_started_the_daemon() {
let made = || {
let mut c = CommandBuilder::new("x");
for k in [
"CLAUDECODE",
"CLAUDE_CODE_CHILD_SESSION",
"NO_COLOR",
"PATH_KEPT",
] {
c.env(k, "1");
}
c
};
let mut c = made();
fresh_env(&mut c, true);
assert!(c.get_env("CLAUDECODE").is_none());
assert!(c.get_env("CLAUDE_CODE_CHILD_SESSION").is_none());
assert!(c.get_env("NO_COLOR").is_none());
assert!(c.get_env("PATH_KEPT").is_some());
let mut c = made();
fresh_env(&mut c, false);
assert!(c.get_env("CLAUDE_CODE_CHILD_SESSION").is_none());
assert!(c.get_env("NO_COLOR").is_some());
}
#[tokio::test]
async fn a_key_buys_one_fast_frame_for_its_echo() {
let dir = crate::store::tempdir::Dir::new("snyvi-pane-echo");
let (events, _) = broadcast::channel(16);
let panes = Panes::new(&dir.path, events);
let live = panes.get("0f0e0d0c0b0a09080706050403020100");
let mut i = live.inner.lock().unwrap();
assert!(!i.echo_due(), "nothing typed");
let key = Instant::now();
i.wrote = key - Duration::from_millis(1);
i.typed = Some(key);
assert!(!i.echo_due(), "output from before the key is not its echo");
i.wrote = key + Duration::from_millis(1);
assert!(i.echo_due(), "the echo");
assert!(!i.echo_due(), "spent: the next output is paced");
i.typed = Some(key - ECHO_WINDOW * 2);
i.wrote = Instant::now();
assert!(!i.echo_due(), "too long after the key to be its echo");
assert_eq!(i.typed, None);
}
#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_unwatched_pane_is_caught_up_when_a_page_attaches() {
let dir = crate::store::tempdir::Dir::new("snyvi-pane-idle");
let (events, _) = broadcast::channel(16);
let panes = Panes::new(&dir.path, events);
let live = panes.get("ffeeddccbbaa99887766554433221100");
let cwd = dir.path.to_string_lossy().to_string();
let said = dir.path.join("late-said");
let cmd = format!(
"printf 'early\\n'; sleep 0.3; printf 'late\\n'; : > '{}'; sleep 2",
said.display()
);
live.start(
Start {
cwd: &cwd,
root: &cwd,
cmd: &cmd,
desk: "d",
slot: 1,
cols: 80,
rows: 10,
accent: "",
offer: false,
env: &[],
},
&panes,
)
.unwrap();
for _ in 0..100 {
if said.exists() {
break;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
assert!(said.exists(), "the shell never printed its second line");
let (first, _rx) = live.attach();
let snap = first.last().unwrap();
assert!(snap.contains("late"), "{snap}");
}
#[cfg(target_os = "linux")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_folder_through_a_link_is_not_a_move() {
let dir = crate::store::tempdir::Dir::new("snyvi-link");
std::fs::create_dir_all(dir.path.join("real")).unwrap();
std::os::unix::fs::symlink(dir.path.join("real"), dir.path.join("link")).unwrap();
let (events, _ev) = broadcast::channel(64);
let panes = Panes::new(&dir.path, events);
let id = "00112233445566778899aabbccddeeff";
let live = panes.get(id);
let link = dir.path.join("link").to_string_lossy().to_string();
live.start(
Start {
cwd: &link,
root: &link,
cmd: "sleep 30",
desk: "d",
slot: 1,
cols: 80,
rows: 10,
accent: "",
offer: false,
env: &[],
},
&panes,
)
.unwrap();
for _ in 0..100 {
if live.shell_cwd().is_some_and(|c| c.ends_with("/real")) {
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(
live.shell_cwd().is_some_and(|c| c.ends_with("/real")),
"the kernel names it resolved"
);
panes.follow_folders();
assert_eq!(live.inner.lock().unwrap().cwd, link, "not a move");
assert!(
live.running_in().is_some_and(|(_, home)| home),
"and still inside the desk's folder"
);
live.stop();
}
#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_agent_state_is_set_on_a_running_pane_once_per_change() {
let dir = crate::store::tempdir::Dir::new("snyvi-agent");
let (events, mut ev) = broadcast::channel(64);
let panes = Panes::new(&dir.path, events);
let id = "ffeeddccbbaa99887766554433221100";
assert!(!panes.set_agent(id, "working"), "a pane nobody opened");
let live = panes.get(id);
assert!(!panes.set_agent(id, "working"), "a stopped pane");
let (_, mut rx) = live.attach();
let cwd = dir.path.to_string_lossy().to_string();
live.start(
Start {
cwd: &cwd,
root: &cwd,
cmd: "read x",
desk: "d",
slot: 1,
cols: 80,
rows: 10,
accent: "",
offer: false,
env: &[],
},
&panes,
)
.unwrap();
assert!(panes.set_agent(id, "needs_you"));
assert!(
panes.status(id).blocked,
"needs_you is blocked, for the sidebar"
);
assert!(panes.set_agent(id, "needs_you"));
assert!(panes.set_agent(id, "nonsense"), "an unknown word clears it");
let st = panes.status(id);
assert_eq!(st.agent, "");
assert!(!st.blocked, "leaving needs_you unblocks");
assert!(panes.set_agent(id, "done"));
let mut said = Vec::new();
while let Ok(Ok(m)) = tokio::time::timeout(Duration::from_millis(200), rx.recv()).await {
let v: serde_json::Value = serde_json::from_str(&m).unwrap();
if v["t"] == "status" && v["s"]["running"] == true {
said.push(v["s"]["agent"].as_str().unwrap().to_string());
}
}
assert_eq!(said, ["", "needs_you", "", "done"], "one frame per change");
let mut dots = Vec::new();
while let Ok(m) = ev.try_recv() {
dots.push(m);
}
assert!(
dots.iter().any(|d| d.contains("\"agent\":\"needs_you\"")),
"{dots:?}"
);
live.stop();
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
while panes.status(id).running && tokio::time::Instant::now() < deadline {
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert_eq!(
panes.status(id).agent,
"",
"the agent goes with its process"
);
}
#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_agent_is_in_from_its_start_to_its_end() {
let dir = crate::store::tempdir::Dir::new("snyvi-agent-in");
let (events, _ev) = broadcast::channel(64);
let panes = Panes::new(&dir.path, events);
let id = "00112233445566778899aabbccddeeff";
assert!(!panes.agent_in(id), "a pane nobody opened");
let live = panes.get(id);
let cwd = dir.path.to_string_lossy().to_string();
live.start(
Start {
cwd: &cwd,
root: &cwd,
cmd: "read x",
desk: "d",
slot: 1,
cols: 80,
rows: 10,
accent: "",
offer: false,
env: &[],
},
&panes,
)
.unwrap();
assert!(!panes.status(id).agent_in);
assert!(panes.agent_in(id));
let st = panes.status(id);
assert!(st.agent_in && st.agent.is_empty(), "in, before any prompt");
assert!(panes.set_agent(id, ""), "SessionEnd");
assert!(!panes.status(id).agent_in, "out, though it never prompted");
assert!(panes.agent_in(id));
assert!(panes.set_agent(id, "working"));
assert!(panes.status(id).agent_in);
live.stop();
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
while panes.status(id).running && tokio::time::Instant::now() < deadline {
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(!panes.status(id).agent_in, "out with its process");
}
#[cfg(unix)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_new_title_reaches_the_desk_and_not_the_sidebar() {
let dir = crate::store::tempdir::Dir::new("snyvi-title");
let (events, mut ev) = broadcast::channel(64);
let panes = Panes::new(&dir.path, events);
let id = "00112233445566778899aabbccddeeff";
let live = panes.get(id);
let (_, mut rx) = live.attach();
let cwd = dir.path.to_string_lossy().to_string();
live.start(
Start {
cwd: &cwd,
root: &cwd,
cmd: "for t in a b c d e; do printf '\\033]0;%s\\007' $t; sleep 0.05; done; printf '\\a'; read x",
desk: "d",
slot: 1,
cols: 80,
rows: 10,
accent: "",
offer: false,
env: &[],
},
&panes,
)
.unwrap();
let mut titles = Vec::new();
let deadline = tokio::time::Instant::now() + Duration::from_secs(20);
while !panes.status(id).blocked && tokio::time::Instant::now() < deadline {
while let Ok(m) = rx.try_recv() {
let v: serde_json::Value = serde_json::from_str(&m).unwrap();
if v["t"] == "status" {
titles.push(v["s"]["title"].as_str().unwrap_or("").to_string());
}
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(panes.status(id).blocked, "the bell rang");
assert!(
titles.iter().any(|t| t == "c"),
"the header hears titles: {titles:?}"
);
let mut dots: Vec<String> = Vec::new();
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
while !dots.iter().any(|m| m.contains("\"blocked\":true"))
&& tokio::time::Instant::now() < deadline
{
while let Ok(m) = ev.try_recv() {
dots.push(m);
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
let s = panes.status(id);
panes.changed(id, &s);
while let Ok(m) = ev.try_recv() {
dots.push(m);
}
assert_eq!(
dots.len(),
2,
"the start and the bell, nothing per title: {dots:?}"
);
assert!(dots[1].contains("\"blocked\":true"), "{dots:?}");
live.stop();
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn a_shell_that_moved_is_found_where_it_went() {
let dir = crate::store::tempdir::Dir::new("snyvi-pane-cwd");
std::fs::create_dir(dir.path.join("sub")).unwrap();
let mut child = std::process::Command::new("sh")
.args(["-c", "cd sub && exec sleep 5"])
.current_dir(&dir.path)
.spawn()
.unwrap();
let want = std::fs::canonicalize(dir.path.join("sub")).unwrap();
let mut seen = None;
for _ in 0..50 {
seen = folder_of(child.id()).map(std::path::PathBuf::from);
if seen.as_deref() == Some(want.as_path()) {
break;
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
let _ = child.kill();
let _ = child.wait();
assert_eq!(seen.as_deref(), Some(want.as_path()));
assert_eq!(folder_of(u32::MAX), None, "no such process, no folder");
}