From c8b0dd9087182837add5ac1073b238e43c9a8430 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 14 Oct 2026 13:45:30 -0700 Subject: [PATCH 1/5] test(cmux-tui): a closed screen reopens with its tab group (cx-gzh.31) Co-Authored-By: Claude Opus 5.5 --- .../reopen_closed_kinds.rs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/cmux-tui/crates/cmux-tui/tests/terminal_host_recovery/reopen_closed_kinds.rs b/cmux-tui/crates/cmux-tui/tests/terminal_host_recovery/reopen_closed_kinds.rs index 8a0fae30d647..b71e2f8263a5 100644 --- a/cmux-tui/crates/cmux-tui/tests/terminal_host_recovery/reopen_closed_kinds.rs +++ b/cmux-tui/crates/cmux-tui/tests/terminal_host_recovery/reopen_closed_kinds.rs @@ -621,6 +621,66 @@ fn a_closed_tab_group_that_emptied_its_screen_reopens_as_its_group() { assert_eq!(groups[0]["tab_ids"], serde_json::Value::Array(back), "{groups:?}"); } +#[test] +fn a_closed_screen_reopens_with_its_tab_group() { + let _exclusive = exclusive_process_test(); + let (harness, mut app, _) = start("reopen-screen-group"); + let workspace = kinds_workspace(&mut app); + let first = workspace["screens"][0]["resource_id"].clone(); + app.send(serde_json::json!({"cmd":"new-screen","workspace":workspace["id"], + "cols":80,"rows":24})); + let screen = kinds_workspace(&mut app)["screens"] + .as_array() + .into_iter() + .flatten() + .find(|screen| screen["resource_id"] != first) + .cloned() + .expect("the new screen is listed"); + let pane = screen["panes"][0]["id"].as_u64().expect("the new screen has a pane"); + for _ in 0..2 { + app.send(serde_json::json!({"cmd":"new-tab","pane":pane})); + } + let tabs = kinds_workspace(&mut app)["screens"] + .as_array() + .into_iter() + .flatten() + .find(|listed| listed["resource_id"] == screen["resource_id"]) + .map(|listed| listed["panes"][0]["tabs"].as_array().cloned().unwrap_or_default()) + .unwrap_or_default() + .iter() + .filter_map(|tab| tab["tab_resource_id"].as_str().map(str::to_string)) + .collect::>(); + assert_eq!(tabs.len(), 3, "{tabs:?}"); + resource_request( + &harness.socket, + "screen-group-create", + "tab_group.create", + serde_json::json!({"machine":"current","session":"current","tabs":tabs[1..], + "name":"Build","color":"blue"}), + Some("screen-group-create"), + ); + let group_id = tab_groups(&harness, "screen-group-list-1")[0]["id"] + .as_str() + .expect("the group has an id") + .to_string(); + + let (item, reopened) = close_records_and_reopens( + &harness, + "screen.close", + serde_json::json!({"screen":screen["resource_id"]}), + "screen-group", + ); + assert_eq!(item["kind"], "screen", "{item}"); + let back = reopened["tab_ids"].as_array().cloned().unwrap_or_default(); + assert_eq!(back.len(), 3, "every tab reopens once: {reopened}"); + let groups = tab_groups(&harness, "screen-group-list-2"); + assert_eq!(groups.len(), 1, "the reopen formed no group: {groups:?}"); + assert_eq!(groups[0]["id"], group_id.as_str()); + assert_eq!(groups[0]["name"], "Build"); + assert_eq!(groups[0]["color"], "blue"); + assert_eq!(groups[0]["tab_ids"], serde_json::json!(back[1..]), "{groups:?}"); +} + fn screen_groups(harness: &RecoveryHarness, request: &str) -> Vec { let reply = resource_request( &harness.socket, From 15f3127aad119cfa1ea5625955013fab0271e87b Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 14 Oct 2026 13:47:36 -0700 Subject: [PATCH 2/5] fix(cmux-tui): closing a screen, pane or workspace keeps its tab groups for reopen (cx-gzh.31) A close records every live tab group holding a closing tab (directly, or in a closing screen or workspace) on its closed-history group, naming the group's closed tabs. Reopen forms each group again around the tabs it brought back, or adds them to the group if it is still open. Close Group now goes through the same capture, which replaces its separate attach step; records stored with the older single tab_group still reopen. Co-Authored-By: Claude Opus 5.5 --- .../cmux-tui-core/src/mux/tab_groups.rs | 3 - .../crates/cmux-tui-core/src/mux/tab_strip.rs | 10 +-- cmux-tui/crates/cmux-tui-core/src/state.rs | 1 + .../cmux-tui-core/src/state/closed_history.rs | 18 +++- .../src/state/closed_history_store.rs | 80 ++++------------- .../src/state/closed_tab_groups.rs | 86 +++++++++++++++++++ 6 files changed, 117 insertions(+), 81 deletions(-) create mode 100644 cmux-tui/crates/cmux-tui-core/src/state/closed_tab_groups.rs diff --git a/cmux-tui/crates/cmux-tui-core/src/mux/tab_groups.rs b/cmux-tui/crates/cmux-tui-core/src/mux/tab_groups.rs index 7c89419d4dca..93d9ecf4a08a 100644 --- a/cmux-tui/crates/cmux-tui-core/src/mux/tab_groups.rs +++ b/cmux-tui/crates/cmux-tui-core/src/mux/tab_groups.rs @@ -753,9 +753,6 @@ impl Mux { .iter() .map(|surface| tab_public_id(state, *surface)) .collect::>>()?; - if let Some(record) = edit.groups.groups.get(&group_id) { - edit.closed_group = Some(group_archive(state, &group_id, record, &tabs)); - } let panes = members .iter() .filter_map(|surface| state.pane_of(*surface)) diff --git a/cmux-tui/crates/cmux-tui-core/src/mux/tab_strip.rs b/cmux-tui/crates/cmux-tui-core/src/mux/tab_strip.rs index 84bf3665287c..5f531f594929 100644 --- a/cmux-tui/crates/cmux-tui-core/src/mux/tab_strip.rs +++ b/cmux-tui/crates/cmux-tui-core/src/mux/tab_strip.rs @@ -75,9 +75,6 @@ pub(crate) struct StripEdit { /// recorded as one closed-history group that forms it again /// (RECOVERABLE-BY-DEFAULT). pub(crate) ungrouped: Option, - /// A group that closes with its tabs (Close Group): its archive joins - /// the closed-history group of those tabs, whose reopen forms it again. - pub(crate) closed_group: Option, pub(crate) result: StripResult, } @@ -108,7 +105,6 @@ impl Mux { tab_state: Vec::new(), saved: None, ungrouped: None, - closed_group: None, result: StripResult::None, }; let result = mutate(&mux, &mut projected, &mut edit)?; @@ -200,8 +196,7 @@ fn strip_state_write( before: &crate::workspace_registry::PresentationSnapshot, edit: StripEdit, ) -> crate::resource_mutation::PlanStateWrite { - let StripEdit { groups, pins, tab_state, saved, ungrouped, closed_group, result: result_kind } = - edit; + let StripEdit { groups, pins, tab_state, saved, ungrouped, result: result_kind } = edit; let mut touched_tabs = BTreeSet::new(); for (tab, group) in &before.tab_groups.members { if groups.members.get(tab) != Some(group) { @@ -244,9 +239,6 @@ fn strip_state_write( archive, )?; } - if let Some(archive) = &closed_group { - crate::state::closed_history_store::attach_closed_tab_group(transaction, archive)?; - } write_tab_groups(transaction, &groups)?; for (tab, pinned, url) in &pins { set_tab_pinned(transaction, tab, *pinned, url.as_deref())?; diff --git a/cmux-tui/crates/cmux-tui-core/src/state.rs b/cmux-tui/crates/cmux-tui-core/src/state.rs index 5e89009a2d58..16003c9f0f91 100644 --- a/cmux-tui/crates/cmux-tui-core/src/state.rs +++ b/cmux-tui/crates/cmux-tui-core/src/state.rs @@ -31,6 +31,7 @@ pub(crate) mod closed_history_store; mod closed_history_tests; #[cfg(test)] mod closed_relaunch_tests; +mod closed_tab_groups; pub(crate) mod command_history_store; pub(crate) mod commit; pub(crate) mod conversation_tabs; diff --git a/cmux-tui/crates/cmux-tui-core/src/state/closed_history.rs b/cmux-tui/crates/cmux-tui-core/src/state/closed_history.rs index 5bd203de92cd..396e3760b49f 100644 --- a/cmux-tui/crates/cmux-tui-core/src/state/closed_history.rs +++ b/cmux-tui/crates/cmux-tui-core/src/state/closed_history.rs @@ -282,11 +282,18 @@ impl Mux { for member in &restore { self.reopen_member(&mutation.actor, &closed_id, member, &mut reopened)?; } - if let Some(group) = record.get("tab_group").filter(|group| group.is_object()) { - // A closed group (Close Group) names its closed tabs: it forms - // again around those this reopen brought back. + // An ungrouped group (`tab_group`, no member) forms again whole. The + // groups a close took (`tab_groups`; `tab_group` on a Close Group + // recorded before cx-gzh.31) name their closed tabs: each forms + // again around those this reopen brought back. + let closed = !restore.is_empty(); + let groups = record + .get("tab_group") + .filter(|group| group.is_object()) + .into_iter() + .chain(record["tab_groups"].as_array().into_iter().flatten()); + for group in groups { let mut group = group.clone(); - let closed = !restore.is_empty(); if closed { let tabs = group["tab_ids"] .as_array() @@ -294,6 +301,9 @@ impl Mux { .flatten() .filter_map(|tab| reopened.reopened_as.get(tab.as_str()?).cloned()) .collect::>(); + if tabs.is_empty() { + continue; + } group["tab_ids"] = serde_json::json!(tabs); } self.reopen_tab_group(&mutation.actor, &group, closed, &mut reopened)?; diff --git a/cmux-tui/crates/cmux-tui-core/src/state/closed_history_store.rs b/cmux-tui/crates/cmux-tui-core/src/state/closed_history_store.rs index 6260801481e0..dad194c13cd5 100644 --- a/cmux-tui/crates/cmux-tui-core/src/state/closed_history_store.rs +++ b/cmux-tui/crates/cmux-tui-core/src/state/closed_history_store.rs @@ -382,11 +382,16 @@ fn insert_group( transaction: &Transaction<'_>, members: Vec, window: Option, + tab_groups: Vec, ) -> anyhow::Result<()> { - let (closed_id, extra) = match take_pending_group(transaction)? { + let (closed_id, mut extra) = match take_pending_group(transaction)? { Some((closed_id, extra)) => (closed_id, Some(extra)), None => (new_closed_id(), None), }; + if !tab_groups.is_empty() { + let fields = extra.get_or_insert_with(|| json!({})); + fields["tab_groups"] = Value::Array(tab_groups); + } insert_record(transaction, closed_id, members, window, extra) } @@ -530,68 +535,6 @@ pub(crate) fn record_ungrouped_group( insert_record(transaction, new_closed_id(), Vec::new(), window, Some(extra)) } -/// Close of a whole tab group: `archive` (as [`record_ungrouped_group`] -/// takes it, naming the closed tabs) rides on the group that recorded its -/// tabs, so reopening them forms the group again. Nothing recorded them -/// (an incognito workspace): nothing to attach. -pub(crate) fn attach_closed_tab_group( - transaction: &Transaction<'_>, - archive: &Value, -) -> anyhow::Result<()> { - let Some(first) = archive["tab_ids"].get(0).and_then(Value::as_str) else { return Ok(()) }; - if !super::store::state_tables_ready(transaction)? { - return Ok(()); - } - let found = transaction - .query_row( - "SELECT g.closed_id, g.record_json FROM closed_groups AS g - WHERE EXISTS (SELECT 1 FROM json_tree(g.record_json, '$.members') AS t - WHERE t.key = 'tab_id' AND t.atom = ?1) - ORDER BY g.seq DESC LIMIT 1", - [first], - |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)), - ) - .optional()?; - let Some((closed_id, record)) = found else { return Ok(()) }; - let mut record: Value = serde_json::from_str(&record)?; - // Only the group this close recorded holds every one of its tabs, as - // tab members or inside the screen the close emptied. - let mut recorded = HashSet::new(); - closed_tab_ids(&record["members"], &mut recorded); - if !archive["tab_ids"] - .as_array() - .into_iter() - .flatten() - .all(|tab| tab.as_str().is_some_and(|tab| recorded.contains(tab))) - { - return Ok(()); - } - record["tab_group"] = archive.clone(); - transaction.execute( - "UPDATE closed_groups SET record_json = ?2 WHERE closed_id = ?1", - params![closed_id, serde_json::to_string(&record)?], - )?; - queue_change(transaction, &state_upsert("closed", &closed_id, public_item(&record))) -} - -/// Every closed tab id in `value`, at any depth. -fn closed_tab_ids<'a>(value: &'a Value, ids: &mut HashSet<&'a str>) { - match value { - Value::Array(items) => items.iter().for_each(|item| closed_tab_ids(item, ids)), - Value::Object(fields) => { - for (key, field) in fields { - match field.as_str() { - Some(id) if key == "tab_id" => { - ids.insert(id); - } - _ => closed_tab_ids(field, ids), - } - } - } - _ => {} - } -} - /// A new closed group id. pub(crate) fn new_closed_id() -> String { format!("closed_{}", new_uuid_v4().replace('-', "")) @@ -628,7 +571,7 @@ pub(crate) fn flush_pending_group(transaction: &Transaction<'_>) -> anyhow::Resu let pending: bool = transaction .query_row("SELECT EXISTS(SELECT 1 FROM closed_group_pending)", [], |row| row.get(0))?; - if pending { insert_group(transaction, Vec::new(), None) } else { Ok(()) } + if pending { insert_group(transaction, Vec::new(), None, Vec::new()) } else { Ok(()) } } /// The highest level among the members: a group with a screen is a screen @@ -770,7 +713,14 @@ pub(crate) fn capture_closed( if capture.members.is_empty() { return Ok(()); } - insert_group(transaction, capture.members, capture.window) + let closing_tabs = closing.tabs.iter().cloned().collect::>(); + let tab_groups = super::closed_tab_groups::closing_tab_groups( + transaction, + &closing_tabs, + &closing_screens, + &closing_workspaces, + )?; + insert_group(transaction, capture.members, capture.window, tab_groups) } fn capture_screen( diff --git a/cmux-tui/crates/cmux-tui-core/src/state/closed_tab_groups.rs b/cmux-tui/crates/cmux-tui-core/src/state/closed_tab_groups.rs new file mode 100644 index 000000000000..558297f23b04 --- /dev/null +++ b/cmux-tui/crates/cmux-tui-core/src/state/closed_tab_groups.rs @@ -0,0 +1,86 @@ +//! The tab groups a close takes with it (cx-gzh.31). Each group with a +//! closing tab rides on the closed-history group as an archive naming its +//! closed tabs, so reopening them forms it again: Close Group, a closed +//! screen, pane or workspace, or the last tab of a group. + +use std::collections::HashSet; + +use rusqlite::{Connection, OptionalExtension}; +use serde_json::{Value, json}; + +/// The archives of the live tab groups holding a tab that closes: one in +/// `tabs`, or in a screen of `screens` or a workspace of `workspaces`. +/// Each names only its closing tabs, in strip order, in the fields +/// `record_ungrouped_group` takes. Groups in an ephemeral workspace are +/// never recorded. +pub(super) fn closing_tab_groups( + connection: &Connection, + tabs: &HashSet, + screens: &HashSet, + workspaces: &HashSet, +) -> anyhow::Result> { + let ready = connection + .query_row( + "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'tab_group_members'", + [], + |_| Ok(()), + ) + .optional()? + .is_some(); + if !ready { + return Ok(Vec::new()); + } + let rows = { + let mut statement = connection.prepare( + "SELECT g.group_id, g.pane_id, g.name, g.color, g.collapsed, g.icon, + m.tab_id, s.public_id, s.workspace_id + FROM tab_group_members AS m + JOIN tab_groups AS g ON g.group_id = m.group_id + JOIN resource_tabs AS t ON t.public_id = m.tab_id AND t.deleted_revision IS NULL + JOIN resource_panes AS p ON p.public_id = t.pane_id + JOIN resource_screens AS s ON s.public_id = p.screen_id + ORDER BY g.group_id ASC, t.position ASC", + )?; + statement + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + row.get::<_, String>(3)?, + row.get::<_, i64>(4)?, + row.get::<_, Option>(5)?, + row.get::<_, String>(6)?, + row.get::<_, String>(7)?, + row.get::<_, String>(8)?, + )) + })? + .collect::, _>>()? + }; + let mut archives: Vec = Vec::new(); + for (group, pane, name, color, collapsed, icon, tab, screen, workspace) in rows { + if !(tabs.contains(&tab) || screens.contains(&screen) || workspaces.contains(&workspace)) + || super::closed_history_store::is_ephemeral(connection, &workspace)? + { + continue; + } + match archives.last_mut().filter(|archive| archive["id"] == group.as_str()) { + Some(archive) => { + if let Some(tab_ids) = archive["tab_ids"].as_array_mut() { + tab_ids.push(json!(tab)); + } + } + None => archives.push(json!({ + "id": group, + "pane_id": pane, + "workspace_id": workspace, + "name": name, + "color": color, + "collapsed": collapsed != 0, + "icon": icon, + "tab_ids": [tab], + })), + } + } + Ok(archives) +} From 9150fae695e71a9a63db9f000a12f08976536f4a Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 14 Oct 2026 14:12:32 -0700 Subject: [PATCH 3/5] fix(cmux-tui): review fixes for captured tab groups (cx-gzh.31) - A group row a screen, pane or workspace close left behind is not live: reopen forms the group again instead of adding to a group whose pane is gone. Every strip edit prunes such rows before it runs, so the id is free again. - Capture the groups before capture_workspace forgets which closing workspaces are ephemeral, so incognito group names are never recorded. - Record only memberships of tabs still in their group's pane. Co-Authored-By: Claude Opus 5.5 --- .../crates/cmux-tui-core/src/mux/tab_strip.rs | 3 +++ .../cmux-tui-core/src/state/closed_history.rs | 6 +++++- .../src/state/closed_history_store.rs | 16 +++++++++------- .../cmux-tui-core/src/state/closed_tab_groups.rs | 1 + 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/cmux-tui/crates/cmux-tui-core/src/mux/tab_strip.rs b/cmux-tui/crates/cmux-tui-core/src/mux/tab_strip.rs index 5f531f594929..2d3310f77b25 100644 --- a/cmux-tui/crates/cmux-tui-core/src/mux/tab_strip.rs +++ b/cmux-tui/crates/cmux-tui-core/src/mux/tab_strip.rs @@ -107,6 +107,9 @@ impl Mux { ungrouped: None, result: StripResult::None, }; + // A screen, pane or workspace close leaves the rows of the + // groups it emptied: drop them before the edit sees them. + prune_tab_groups(&projected, &mut edit.groups); let result = mutate(&mux, &mut projected, &mut edit)?; Mux::rebuild_split_screen_index(&mut projected); prune_tab_groups(&projected, &mut edit.groups); diff --git a/cmux-tui/crates/cmux-tui-core/src/state/closed_history.rs b/cmux-tui/crates/cmux-tui-core/src/state/closed_history.rs index 396e3760b49f..1ca836f1b45a 100644 --- a/cmux-tui/crates/cmux-tui-core/src/state/closed_history.rs +++ b/cmux-tui/crates/cmux-tui-core/src/state/closed_history.rs @@ -370,7 +370,11 @@ impl Mux { ) -> anyhow::Result<()> { let Some(id) = archive["id"].as_str().map(str::to_string) else { return Ok(()) }; let presentation = self.presentation_snapshot(); - let live = presentation.tab_groups.groups.contains_key(&id); + // A group row a close left behind (no strip edit pruned it yet) + // holds no open tab: it is not live, and forms again. + let live = self.with_state(|state| { + !crate::mux::tab_groups::group_members(state, &presentation.tab_groups, &id).is_empty() + }); if !live || closed { let wanted = archive["tab_ids"] .as_array() diff --git a/cmux-tui/crates/cmux-tui-core/src/state/closed_history_store.rs b/cmux-tui/crates/cmux-tui-core/src/state/closed_history_store.rs index dad194c13cd5..4b5bdc39c4d4 100644 --- a/cmux-tui/crates/cmux-tui-core/src/state/closed_history_store.rs +++ b/cmux-tui/crates/cmux-tui-core/src/state/closed_history_store.rs @@ -684,6 +684,15 @@ pub(crate) fn capture_closed( } let closing_workspaces = closing.workspaces.iter().cloned().collect::>(); let closing_screens = closing.screens.iter().cloned().collect::>(); + // Before `capture_workspace` forgets which closing workspaces are + // ephemeral: their groups are never recorded. + let closing_tabs = closing.tabs.iter().cloned().collect::>(); + let tab_groups = super::closed_tab_groups::closing_tab_groups( + transaction, + &closing_tabs, + &closing_screens, + &closing_workspaces, + )?; let mut capture = Capture::default(); for workspace_id in &closing.workspaces { capture_workspace(transaction, workspace_id, &mut capture)?; @@ -713,13 +722,6 @@ pub(crate) fn capture_closed( if capture.members.is_empty() { return Ok(()); } - let closing_tabs = closing.tabs.iter().cloned().collect::>(); - let tab_groups = super::closed_tab_groups::closing_tab_groups( - transaction, - &closing_tabs, - &closing_screens, - &closing_workspaces, - )?; insert_group(transaction, capture.members, capture.window, tab_groups) } diff --git a/cmux-tui/crates/cmux-tui-core/src/state/closed_tab_groups.rs b/cmux-tui/crates/cmux-tui-core/src/state/closed_tab_groups.rs index 558297f23b04..8e64e88a8781 100644 --- a/cmux-tui/crates/cmux-tui-core/src/state/closed_tab_groups.rs +++ b/cmux-tui/crates/cmux-tui-core/src/state/closed_tab_groups.rs @@ -37,6 +37,7 @@ pub(super) fn closing_tab_groups( FROM tab_group_members AS m JOIN tab_groups AS g ON g.group_id = m.group_id JOIN resource_tabs AS t ON t.public_id = m.tab_id AND t.deleted_revision IS NULL + AND t.pane_id = g.pane_id JOIN resource_panes AS p ON p.public_id = t.pane_id JOIN resource_screens AS s ON s.public_id = p.screen_id ORDER BY g.group_id ASC, t.position ASC", From 0694a81d6e00339dc03b79f72d2555d190d64f1b Mon Sep 17 00:00:00 2001 From: Leo Li Date: Wed, 14 Oct 2026 14:35:08 -0700 Subject: [PATCH 4/5] fix(cmux-tui): import group_members (clippy unnecessary qualification) Co-Authored-By: Claude Opus 5.5 --- cmux-tui/crates/cmux-tui-core/src/state/closed_history.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cmux-tui/crates/cmux-tui-core/src/state/closed_history.rs b/cmux-tui/crates/cmux-tui-core/src/state/closed_history.rs index 1ca836f1b45a..445f0e8a7b02 100644 --- a/cmux-tui/crates/cmux-tui-core/src/state/closed_history.rs +++ b/cmux-tui/crates/cmux-tui-core/src/state/closed_history.rs @@ -13,7 +13,7 @@ //! request's result, so a retry with the same key replays it. use crate::Actor; -use crate::mux::tab_groups::pane_by_public_id; +use crate::mux::tab_groups::{group_members, pane_by_public_id}; use crate::mux::tab_strip::StripRequest; use crate::mux::*; use crate::state::closed_history_query::{ @@ -372,9 +372,8 @@ impl Mux { let presentation = self.presentation_snapshot(); // A group row a close left behind (no strip edit pruned it yet) // holds no open tab: it is not live, and forms again. - let live = self.with_state(|state| { - !crate::mux::tab_groups::group_members(state, &presentation.tab_groups, &id).is_empty() - }); + let live = self + .with_state(|state| !group_members(state, &presentation.tab_groups, &id).is_empty()); if !live || closed { let wanted = archive["tab_ids"] .as_array() From f8a71c3e805bfab49daaad2dc6b6b9a546aab573 Mon Sep 17 00:00:00 2001 From: "cmux-autofix[bot]" Date: Wed, 14 Oct 2026 23:17:28 +0000 Subject: [PATCH 5/5] chore(next): PR autofix at 52a293c07154 - cargo fmt in Native/OptChat/optchat-chief Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38652086305 --- .../optchat-chief/tests/shared_acpmux.rs | 110 +++++++++++++----- 1 file changed, 84 insertions(+), 26 deletions(-) diff --git a/Native/OptChat/optchat-chief/tests/shared_acpmux.rs b/Native/OptChat/optchat-chief/tests/shared_acpmux.rs index e9a57153c8bd..3442ea3dc14d 100644 --- a/Native/OptChat/optchat-chief/tests/shared_acpmux.rs +++ b/Native/OptChat/optchat-chief/tests/shared_acpmux.rs @@ -89,14 +89,26 @@ fn session(id: &str, name: &str, tags: serde_json::Value) -> serde_json::Value { fn sessions(a: &Path, b: &Path) -> serde_json::Value { let (ia, ib) = (home_id(a), home_id(b)); json!([ - session("s-a-turn", &format!("optchat-{ia}-turn"), - json!({"cmux.chief": ia, "cmux.chief.role": "turn"})), - session("s-a-sub", &format!("optchat-sub-{ia}-a1"), - json!({"mux.parent": format!("optchat-chief:{ia}")})), - session("s-b-turn", &format!("optchat-{ib}-turn"), - json!({"cmux.chief": ib, "cmux.chief.role": "turn"})), - session("s-b-sub", &format!("optchat-sub-{ib}-a1"), - json!({"mux.parent": format!("optchat-chief:{ib}")})), + session( + "s-a-turn", + &format!("optchat-{ia}-turn"), + json!({"cmux.chief": ia, "cmux.chief.role": "turn"}) + ), + session( + "s-a-sub", + &format!("optchat-sub-{ia}-a1"), + json!({"mux.parent": format!("optchat-chief:{ia}")}) + ), + session( + "s-b-turn", + &format!("optchat-{ib}-turn"), + json!({"cmux.chief": ib, "cmux.chief.role": "turn"}) + ), + session( + "s-b-sub", + &format!("optchat-sub-{ib}-a1"), + json!({"mux.parent": format!("optchat-chief:{ib}")}) + ), session("s-person", "my-chat", json!({})), ]) } @@ -104,7 +116,10 @@ fn sessions(a: &Path, b: &Path) -> serde_json::Value { fn wait_for(what: &str, limit: Duration, mut done: impl FnMut() -> bool) { let deadline = Instant::now() + limit; while !done() { - assert!(Instant::now() < deadline, "{what} did not happen within {limit:?}"); + assert!( + Instant::now() < deadline, + "{what} did not happen within {limit:?}" + ); std::thread::sleep(Duration::from_millis(50)); // test harness: poll a file the fake writes } } @@ -156,7 +171,11 @@ fn two_chiefs_on_one_acpmux_reach_only_their_own_sessions() { let (a, b) = (chief_home.join("c/aaaa1111"), chief_home.join("c/bbbb2222")); let acpmux_home = chief_home.join("acpmux"); let bin = fake_acpmux(&acpmux_home); - std::fs::write(acpmux_home.join("sessions.json"), sessions(&a, &b).to_string()).unwrap(); + std::fs::write( + acpmux_home.join("sessions.json"), + sessions(&a, &b).to_string(), + ) + .unwrap(); let socket = acpmux_home.join("acpmux.sock"); let mut daemon = Command::new(&bin) .args(["daemon", "run"]) @@ -174,9 +193,18 @@ fn two_chiefs_on_one_acpmux_reach_only_their_own_sessions() { let listed = chief(&a, &acpmux_home, &bin, &["agents", "list"]); let text = String::from_utf8_lossy(&listed.stdout); assert!(listed.status.success(), "{listed:?}"); - assert!(text.contains(&format!("optchat-sub-{}-a1", home_id(&a))), "{text}"); - assert!(!text.contains(&home_id(&b)), "Chief A listed Chief B's session: {text}"); - assert!(!text.contains("my-chat"), "Chief A listed the person's session: {text}"); + assert!( + text.contains(&format!("optchat-sub-{}-a1", home_id(&a))), + "{text}" + ); + assert!( + !text.contains(&home_id(&b)), + "Chief A listed Chief B's session: {text}" + ); + assert!( + !text.contains("my-chat"), + "Chief A listed the person's session: {text}" + ); // Chief A cannot send to, allow or deny Chief B's sessions, by name or id. let b_sub = format!("optchat-sub-{}-a1", home_id(&b)); @@ -187,10 +215,17 @@ fn two_chiefs_on_one_acpmux_reach_only_their_own_sessions() { vec!["agents", "deny", target], ] { let out = chief(&a, &acpmux_home, &bin, &args); - assert!(!out.status.success(), "{args:?} reached another Chief: {out:?}"); + assert!( + !out.status.success(), + "{args:?} reached another Chief: {out:?}" + ); } } - assert!(calls(&acpmux_home).is_empty(), "requests reached other sessions: {:?}", calls(&acpmux_home)); + assert!( + calls(&acpmux_home).is_empty(), + "requests reached other sessions: {:?}", + calls(&acpmux_home) + ); // Ending Chief A ends its turn and its child only; the daemon stays. let ended = chief(&a, &acpmux_home, &bin, &["end-sessions"]); @@ -198,7 +233,10 @@ fn two_chiefs_on_one_acpmux_reach_only_their_own_sessions() { let mut got = calls(&acpmux_home); got.sort(); assert_eq!(got, ["_acpmux/kill s-a-sub", "_acpmux/kill s-a-turn"]); - assert!(daemon.try_wait().unwrap().is_none(), "the shared daemon was stopped"); + assert!( + daemon.try_wait().unwrap().is_none(), + "the shared daemon was stopped" + ); let _ = daemon.kill(); let _ = daemon.wait(); } @@ -231,7 +269,9 @@ fn host_stops(dir: &Path, mux_home: &Path, acpmux_home: &Path, shared: Option<&P // stays the default: a non-default one needs the owner daemon to start). command.env("MUX_CHIEF_TITLE", "Test Chief"); if let Some(chief_home) = shared { - command.env("OPTCHAT_ACPMUX_SHARED", "1").env("CMUX_CHIEF_HOME", chief_home); + command + .env("OPTCHAT_ACPMUX_SHARED", "1") + .env("CMUX_CHIEF_HOME", chief_home); } let mut host = command.spawn().unwrap(); let mut guard = Leftovers(vec![host.id() as i32]); @@ -239,7 +279,11 @@ fn host_stops(dir: &Path, mux_home: &Path, acpmux_home: &Path, shared: Option<&P wait_for("the host starting acpmux", Duration::from_secs(30), || { std::fs::read_to_string(&pid_file).is_ok_and(|p| !p.is_empty()) }); - let fake: i32 = std::fs::read_to_string(&pid_file).unwrap().trim().parse().unwrap(); + let fake: i32 = std::fs::read_to_string(&pid_file) + .unwrap() + .trim() + .parse() + .unwrap(); guard.0.push(fake); // SAFETY: the pid is our own child. unsafe { libc::kill(host.id() as i32, libc::SIGTERM) }; @@ -262,7 +306,11 @@ fn a_shared_host_leaves_the_chief_homes_acpmux_running_and_ends_only_its_session let (mine, other) = (chief_home.join("c/aaaa1111"), chief_home.join("c/bbbb2222")); let acpmux_home = chief_home.join("acpmux"); std::fs::create_dir_all(&acpmux_home).unwrap(); - std::fs::write(acpmux_home.join("sessions.json"), sessions(&mine, &other).to_string()).unwrap(); + std::fs::write( + acpmux_home.join("sessions.json"), + sessions(&mine, &other).to_string(), + ) + .unwrap(); let fake = host_stops(dir.path(), &mine, &acpmux_home, Some(&chief_home)); let _guard = Leftovers(vec![fake]); assert_eq!( @@ -273,10 +321,9 @@ fn a_shared_host_leaves_the_chief_homes_acpmux_running_and_ends_only_its_session // None of the starting Chief's identity reaches the daemon (its // sessions would act as that Chief): it looks as if the default Chief's // host had started it. - let identity: serde_json::Value = serde_json::from_str( - &std::fs::read_to_string(acpmux_home.join("identity.json")).unwrap(), - ) - .unwrap(); + let identity: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(acpmux_home.join("identity.json")).unwrap()) + .unwrap(); assert_eq!( identity, json!({"MUX_HOME": chief_home.display().to_string(), @@ -285,8 +332,15 @@ fn a_shared_host_leaves_the_chief_homes_acpmux_running_and_ends_only_its_session ); let mut got = calls(&acpmux_home); got.sort(); - assert_eq!(got, ["_acpmux/kill s-a-sub", "_acpmux/kill s-a-turn"], "no shutdown, own sessions only"); - assert!(alive(fake), "the shared acpmux stopped with one Chief's host"); + assert_eq!( + got, + ["_acpmux/kill s-a-sub", "_acpmux/kill s-a-turn"], + "no shutdown, own sessions only" + ); + assert!( + alive(fake), + "the shared acpmux stopped with one Chief's host" + ); } #[test] @@ -301,6 +355,10 @@ fn a_host_with_its_own_acpmux_home_keeps_its_own_daemon() { mux_home.display().to_string(), "its own daemon's Chief home is its MUX_HOME" ); - assert_eq!(calls(&acpmux_home), ["shutdown"], "it shuts the daemon it started down"); + assert_eq!( + calls(&acpmux_home), + ["shutdown"], + "it shuts the daemon it started down" + ); wait_for("its daemon ending", Duration::from_secs(5), || !alive(fake)); }