@@ -430,6 +430,81 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
430430 } ) ) ;
431431}
432432
433+ #[ tokio:: test]
434+ async fn multi_agent_v2_send_message_accepts_root_target_from_child ( ) {
435+ let ( mut session, mut turn) = make_session_and_context ( ) . await ;
436+ let manager = thread_manager ( ) ;
437+ let root = manager
438+ . start_thread ( ( * turn. config ) . clone ( ) )
439+ . await
440+ . expect ( "root thread should start" ) ;
441+ session. services . agent_control = manager. agent_control ( ) ;
442+ session. conversation_id = root. thread_id ;
443+ let mut config = ( * turn. config ) . clone ( ) ;
444+ config
445+ . features
446+ . enable ( Feature :: MultiAgentV2 )
447+ . expect ( "test config should allow feature update" ) ;
448+ turn. config = Arc :: new ( config) ;
449+
450+ let child_path = AgentPath :: try_from ( "/root/worker" ) . expect ( "agent path" ) ;
451+ let child_thread_id = session
452+ . services
453+ . agent_control
454+ . spawn_agent_with_metadata (
455+ ( * turn. config ) . clone ( ) ,
456+ vec ! [ UserInput :: Text {
457+ text: "inspect this repo" . to_string( ) ,
458+ text_elements: Vec :: new( ) ,
459+ } ] ,
460+ Some ( SessionSource :: SubAgent ( SubAgentSource :: ThreadSpawn {
461+ parent_thread_id : root. thread_id ,
462+ depth : 1 ,
463+ agent_path : Some ( child_path. clone ( ) ) ,
464+ agent_nickname : None ,
465+ agent_role : None ,
466+ } ) ) ,
467+ crate :: agent:: control:: SpawnAgentOptions :: default ( ) ,
468+ )
469+ . await
470+ . expect ( "worker spawn should succeed" )
471+ . thread_id ;
472+ session. conversation_id = child_thread_id;
473+ turn. session_source = SessionSource :: SubAgent ( SubAgentSource :: ThreadSpawn {
474+ parent_thread_id : root. thread_id ,
475+ depth : 1 ,
476+ agent_path : Some ( child_path. clone ( ) ) ,
477+ agent_nickname : None ,
478+ agent_role : None ,
479+ } ) ;
480+
481+ SendMessageHandlerV2
482+ . handle ( invocation (
483+ Arc :: new ( session) ,
484+ Arc :: new ( turn) ,
485+ "send_message" ,
486+ function_payload ( json ! ( {
487+ "target" : "/root" ,
488+ "items" : [ { "type" : "text" , "text" : "done" } ]
489+ } ) ) ,
490+ ) )
491+ . await
492+ . expect ( "send_message should accept the root agent path" ) ;
493+
494+ assert ! ( manager. captured_ops( ) . iter( ) . any( |( id, op) | {
495+ * id == root. thread_id
496+ && matches!(
497+ op,
498+ Op :: InterAgentCommunication { communication }
499+ if communication. author == child_path
500+ && communication. recipient == AgentPath :: root( )
501+ && communication. other_recipients. is_empty( )
502+ && communication. content == "done"
503+ && !communication. trigger_turn
504+ )
505+ } ) ) ;
506+ }
507+
433508#[ tokio:: test]
434509async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_message ( ) {
435510 let ( mut session, mut turn) = make_session_and_context ( ) . await ;
@@ -496,11 +571,20 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa
496571 let result: ListAgentsResult =
497572 serde_json:: from_str ( & content) . expect ( "list_agents result should be json" ) ;
498573
499- assert_eq ! ( result. agents. len( ) , 1 ) ;
500- assert_eq ! ( result. agents[ 0 ] . agent_name, "/root/worker" ) ;
501- assert_eq ! ( result. agents[ 0 ] . agent_status, json!( { "completed" : "done" } ) ) ;
574+ let agent_names = result
575+ . agents
576+ . iter ( )
577+ . map ( |agent| agent. agent_name . as_str ( ) )
578+ . collect :: < Vec < _ > > ( ) ;
579+ assert_eq ! ( agent_names, vec![ "/root" , "/root/worker" ] ) ;
580+ let worker = result
581+ . agents
582+ . iter ( )
583+ . find ( |agent| agent. agent_name == "/root/worker" )
584+ . expect ( "worker agent should be listed" ) ;
585+ assert_eq ! ( worker. agent_status, json!( { "completed" : "done" } ) ) ;
502586 assert_eq ! (
503- result . agents [ 0 ] . last_task_message. as_deref( ) ,
587+ worker . last_task_message. as_deref( ) ,
504588 Some ( "inspect this repo" )
505589 ) ;
506590 assert_eq ! ( success, Some ( true ) ) ;
@@ -647,7 +731,8 @@ async fn multi_agent_v2_list_agents_omits_closed_agents() {
647731 let result: ListAgentsResult =
648732 serde_json:: from_str ( & content) . expect ( "list_agents result should be json" ) ;
649733
650- assert ! ( result. agents. is_empty( ) ) ;
734+ assert_eq ! ( result. agents. len( ) , 1 ) ;
735+ assert_eq ! ( result. agents[ 0 ] . agent_name, "/root" ) ;
651736}
652737
653738#[ tokio:: test]
@@ -2036,6 +2121,54 @@ async fn multi_agent_v2_close_agent_accepts_task_name_target() {
20362121 ) ;
20372122}
20382123
2124+ #[ tokio:: test]
2125+ async fn multi_agent_v2_close_agent_rejects_root_target_and_id ( ) {
2126+ let ( mut session, mut turn) = make_session_and_context ( ) . await ;
2127+ let manager = thread_manager ( ) ;
2128+ let root = manager
2129+ . start_thread ( ( * turn. config ) . clone ( ) )
2130+ . await
2131+ . expect ( "root thread should start" ) ;
2132+ session. services . agent_control = manager. agent_control ( ) ;
2133+ session. conversation_id = root. thread_id ;
2134+ let mut config = ( * turn. config ) . clone ( ) ;
2135+ config
2136+ . features
2137+ . enable ( Feature :: MultiAgentV2 )
2138+ . expect ( "test config should allow feature update" ) ;
2139+ turn. config = Arc :: new ( config) ;
2140+
2141+ let session = Arc :: new ( session) ;
2142+ let turn = Arc :: new ( turn) ;
2143+ let root_path_error = CloseAgentHandlerV2
2144+ . handle ( invocation (
2145+ session. clone ( ) ,
2146+ turn. clone ( ) ,
2147+ "close_agent" ,
2148+ function_payload ( json ! ( { "target" : "/root" } ) ) ,
2149+ ) )
2150+ . await
2151+ . expect_err ( "close_agent should reject the root path" ) ;
2152+ assert_eq ! (
2153+ root_path_error,
2154+ FunctionCallError :: RespondToModel ( "root is not a spawned agent" . to_string( ) )
2155+ ) ;
2156+
2157+ let root_id_error = CloseAgentHandlerV2
2158+ . handle ( invocation (
2159+ session,
2160+ turn,
2161+ "close_agent" ,
2162+ function_payload ( json ! ( { "target" : root. thread_id. to_string( ) } ) ) ,
2163+ ) )
2164+ . await
2165+ . expect_err ( "close_agent should reject the root thread id" ) ;
2166+ assert_eq ! (
2167+ root_id_error,
2168+ FunctionCallError :: RespondToModel ( "root is not a spawned agent" . to_string( ) )
2169+ ) ;
2170+ }
2171+
20392172#[ tokio:: test]
20402173async fn close_agent_submits_shutdown_and_returns_previous_status ( ) {
20412174 let ( mut session, turn) = make_session_and_context ( ) . await ;
0 commit comments