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