From df22d9be21c5cbe589f0156d913c05a8f770cf43 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 16 Aug 2026 07:55:15 +0800 Subject: [PATCH] test(e2e): pin the tree and the registry agreeing under a race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Twelve `tab new` at once against one workspace, then check that every tab landed exactly once and that the tree's pane count still matches the registry's. Both invariants this leans on are invisible to a single-threaded test. The store takes `notify_order` before its state lock and holds it across delivery, so subscribers see mutations in the order they happened; and each `tab new` spawns its pane before the tree is asked to hold it, so a refusal in between leaves a pane running that nothing references. That second one is quiet — it shows up only as these two counts disagreeing, which is what `pane close --orphans` exists to mop up. Verified by hand first: twelve racing creates, and a mixed race of splits, tab creates, renames and closes, both left the counts equal with no orphans; the same held with a GUI mirroring the changes live, whose log recorded no resync or divergence. This is that check, kept. Also measured while here, and sound, so it is not re-run: 180 pane create/close cycles move the daemon from 10 fds and 5 threads to 13 and 7, and then stay there across two further rounds — one-time overhead, not a leak. --- crates/tty7-cli/tests/cli_e2e.rs | 53 ++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/crates/tty7-cli/tests/cli_e2e.rs b/crates/tty7-cli/tests/cli_e2e.rs index 7283b928..9253ac25 100644 --- a/crates/tty7-cli/tests/cli_e2e.rs +++ b/crates/tty7-cli/tests/cli_e2e.rs @@ -51,6 +51,10 @@ fn main() { run_keep_files_the_pane_so_ls_shows_it, ), ("send_then_capture_round_trip", send_then_capture_round_trip), + ( + "concurrent_tab_creates_leave_the_tree_and_registry_agreeing", + concurrent_tab_creates_leave_the_tree_and_registry_agreeing, + ), ( "send_enter_submits_in_a_paste_aware_raw_mode_tui", send_enter_submits_in_a_paste_aware_raw_mode_tui, @@ -500,6 +504,55 @@ fn run_streams_output_and_passes_the_exit_code(daemon: &Daemon) { ); } +/// Twelve clients adding a tab to one workspace at once, and every pane +/// accounted for afterwards. +/// +/// The store takes `notify_order` before its state lock and holds it across +/// delivery so subscribers see mutations in the order they happened; each +/// `tab new` also spawns its pane before the tree is asked to hold it. Neither +/// is visible from a single-threaded test, and the failure they guard against +/// is quiet: a pane the registry runs that no tree references shows up only as +/// these two counts disagreeing, which is exactly what `pane close --orphans` +/// exists to mop up. +fn concurrent_tab_creates_leave_the_tree_and_registry_agreeing(daemon: &Daemon) { + const RACERS: usize = 12; + + let ws = daemon.run_json(&["ws", "new", "racews"]); + let ws_id = ws["id"].as_str().expect("ws new prints the id").to_string(); + + std::thread::scope(|scope| { + for _ in 0..RACERS { + let ws_id = ws_id.clone(); + scope.spawn(move || { + let out = daemon.run(&["tab", "new", &ws_id]); + assert!( + out.status.success(), + "a racing `tab new` failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + }); + } + }); + + let tabs = daemon.run_json(&["tab", "ls", &ws_id]); + let tabs = tabs["tabs"].as_array().expect("tab ls lists tabs").len(); + assert_eq!(tabs, RACERS, "every racing tab has to land, and land once"); + + let listed = daemon.run_json(&["ls"]); + let tree: u64 = listed["workspaces"] + .as_array() + .expect("ls lists workspaces") + .iter() + .map(|w| w["panes"].as_u64().unwrap_or_default()) + .sum(); + let running = daemon.run_json(&["pane", "ls", "--all"]); + let running = running["panes"].as_array().expect("pane ls --all").len() as u64; + assert_eq!( + tree, running, + "the tree and the registry disagree, so a pane is running that nothing holds" + ); +} + fn run_keep_files_the_pane_so_ls_shows_it(daemon: &Daemon) { let ws = daemon.run_json(&["ws", "new", "runws"]); let ws_id = ws["id"].as_str().expect("ws new prints the id").to_string();