Skip to content

Commit eaf499c

Browse files
committed
fix(workspaces): skip reconnect when already connected
Avoid spawning when a workspace session already exists and add tests for both connected and unconnected connect paths.
1 parent e2a4986 commit eaf499c

1 file changed

Lines changed: 131 additions & 0 deletions

File tree

src-tauri/src/shared/workspaces_core/connect.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ where
2525
Fut: Future<Output = Result<Arc<WorkspaceSession>, String>>,
2626
{
2727
let (entry, parent_entry) = resolve_entry_and_parent(workspaces, &workspace_id).await?;
28+
{
29+
let sessions = sessions.lock().await;
30+
if sessions.contains_key(&entry.id) {
31+
return Ok(());
32+
}
33+
}
2834
let (default_bin, codex_args) = {
2935
let settings = app_settings.lock().await;
3036
(
@@ -47,3 +53,128 @@ pub(super) async fn kill_session_by_id(
4753
kill_child_process_tree(&mut child).await;
4854
}
4955
}
56+
57+
#[cfg(test)]
58+
mod tests {
59+
use super::*;
60+
61+
use std::collections::HashMap;
62+
use std::process::Stdio;
63+
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
64+
use std::sync::Arc;
65+
66+
use tokio::process::Command;
67+
use tokio::sync::Mutex;
68+
69+
use crate::types::{WorkspaceKind, WorkspaceSettings};
70+
71+
fn make_workspace_entry(id: &str) -> WorkspaceEntry {
72+
WorkspaceEntry {
73+
id: id.to_string(),
74+
name: id.to_string(),
75+
path: "/tmp".to_string(),
76+
codex_bin: None,
77+
kind: WorkspaceKind::Main,
78+
parent_id: None,
79+
worktree: None,
80+
settings: WorkspaceSettings::default(),
81+
}
82+
}
83+
84+
fn make_session(entry: WorkspaceEntry) -> Arc<WorkspaceSession> {
85+
let mut cmd = if cfg!(windows) {
86+
let mut cmd = Command::new("cmd");
87+
cmd.args(["/C", "more"]);
88+
cmd
89+
} else {
90+
let mut cmd = Command::new("sh");
91+
cmd.args(["-c", "cat"]);
92+
cmd
93+
};
94+
95+
cmd.stdin(Stdio::piped())
96+
.stdout(Stdio::null())
97+
.stderr(Stdio::null());
98+
99+
let mut child = cmd.spawn().expect("spawn dummy child");
100+
let stdin = child.stdin.take().expect("dummy child stdin");
101+
102+
Arc::new(WorkspaceSession {
103+
entry,
104+
codex_args: None,
105+
child: Mutex::new(child),
106+
stdin: Mutex::new(stdin),
107+
pending: Mutex::new(HashMap::new()),
108+
next_id: AtomicU64::new(0),
109+
background_thread_callbacks: Mutex::new(HashMap::new()),
110+
})
111+
}
112+
113+
#[test]
114+
fn connect_workspace_is_noop_when_already_connected() {
115+
tokio::runtime::Runtime::new().unwrap().block_on(async {
116+
let entry = make_workspace_entry("ws-1");
117+
let workspaces = Mutex::new(HashMap::from([(entry.id.clone(), entry.clone())]));
118+
let sessions = Mutex::new(HashMap::from([(
119+
entry.id.clone(),
120+
make_session(entry.clone()),
121+
)]));
122+
let app_settings = Mutex::new(AppSettings::default());
123+
let spawn_calls = Arc::new(AtomicUsize::new(0));
124+
let spawn_calls_ref = spawn_calls.clone();
125+
126+
connect_workspace_core(
127+
entry.id.clone(),
128+
&workspaces,
129+
&sessions,
130+
&app_settings,
131+
move |_entry, _default_bin, _codex_args, _codex_home| {
132+
let spawn_calls_ref = spawn_calls_ref.clone();
133+
async move {
134+
spawn_calls_ref.fetch_add(1, Ordering::SeqCst);
135+
Err("should not spawn".to_string())
136+
}
137+
},
138+
)
139+
.await
140+
.expect("connect should be noop");
141+
142+
assert_eq!(spawn_calls.load(Ordering::SeqCst), 0);
143+
kill_session_by_id(&sessions, &entry.id).await;
144+
});
145+
}
146+
147+
#[test]
148+
fn connect_workspace_spawns_when_not_connected() {
149+
tokio::runtime::Runtime::new().unwrap().block_on(async {
150+
let entry = make_workspace_entry("ws-2");
151+
let workspaces = Mutex::new(HashMap::from([(entry.id.clone(), entry.clone())]));
152+
let sessions = Mutex::new(HashMap::<String, Arc<WorkspaceSession>>::new());
153+
let app_settings = Mutex::new(AppSettings::default());
154+
let spawn_calls = Arc::new(AtomicUsize::new(0));
155+
let spawn_calls_ref = spawn_calls.clone();
156+
let entry_for_spawn = entry.clone();
157+
158+
connect_workspace_core(
159+
entry.id.clone(),
160+
&workspaces,
161+
&sessions,
162+
&app_settings,
163+
move |_entry, _default_bin, _codex_args, _codex_home| {
164+
let spawn_calls_ref = spawn_calls_ref.clone();
165+
let entry_for_spawn = entry_for_spawn.clone();
166+
async move {
167+
spawn_calls_ref.fetch_add(1, Ordering::SeqCst);
168+
Ok(make_session(entry_for_spawn))
169+
}
170+
},
171+
)
172+
.await
173+
.expect("connect should spawn");
174+
175+
assert_eq!(spawn_calls.load(Ordering::SeqCst), 1);
176+
assert!(sessions.lock().await.contains_key(&entry.id));
177+
kill_session_by_id(&sessions, &entry.id).await;
178+
});
179+
}
180+
}

0 commit comments

Comments
 (0)