@@ -907,4 +907,158 @@ mod tests {
907907 let result = svc. upsert_config ( 1 , request) . await ;
908908 assert ! ( result. is_ok( ) ) ;
909909 }
910+
911+ // ---------------------------------------------------------------------------
912+ // Feature: sandbox_enabled as Option<bool> in config service
913+ // ---------------------------------------------------------------------------
914+
915+ #[ tokio:: test]
916+ async fn test_upsert_sandbox_enabled_none_stored_correctly ( ) {
917+ // sandbox_enabled: None means "use global default" — must be stored as NULL
918+ let mut expected = make_config ( 1 ) ;
919+ expected. sandbox_enabled = None ; // model returned by mock DB reflects what was stored
920+
921+ let db = MockDatabase :: new ( DatabaseBackend :: Postgres )
922+ . append_query_results ( vec ! [ Vec :: <project_agents:: Model >:: new( ) ] ) // no existing config
923+ . append_query_results ( vec ! [ vec![ expected. clone( ) ] ] ) // insert returns model
924+ . into_connection ( ) ;
925+ let svc = AgentConfigService :: new ( Arc :: new ( db) , make_encryption_service ( ) ) ;
926+
927+ let request = UpsertAgentRequest {
928+ enabled : Some ( false ) ,
929+ sandbox_enabled : None , // explicit None
930+ ai_provider : None ,
931+ api_key : None ,
932+ ai_provider_key_id : None ,
933+ daily_budget_cents : None ,
934+ max_turns : None ,
935+ cooldown_minutes : None ,
936+ trigger_config : None ,
937+ prompt : None ,
938+ timeout_seconds : None ,
939+ deliverable : None ,
940+ slug : None ,
941+ name : None ,
942+ description : None ,
943+ branch_prefix : None ,
944+ } ;
945+
946+ let result = svc. upsert_config ( 1 , request) . await ;
947+ assert ! (
948+ result. is_ok( ) ,
949+ "upsert with sandbox_enabled=None should succeed"
950+ ) ;
951+ let model = result. unwrap ( ) ;
952+ assert ! (
953+ model. sandbox_enabled. is_none( ) ,
954+ "sandbox_enabled should be None in the returned model"
955+ ) ;
956+ }
957+
958+ #[ tokio:: test]
959+ async fn test_upsert_sandbox_enabled_some_true_stored_correctly ( ) {
960+ let mut expected = make_config ( 1 ) ;
961+ expected. sandbox_enabled = Some ( true ) ;
962+
963+ let db = MockDatabase :: new ( DatabaseBackend :: Postgres )
964+ . append_query_results ( vec ! [ Vec :: <project_agents:: Model >:: new( ) ] ) // no existing config
965+ . append_query_results ( vec ! [ vec![ expected. clone( ) ] ] ) // insert returns model
966+ . into_connection ( ) ;
967+ let svc = AgentConfigService :: new ( Arc :: new ( db) , make_encryption_service ( ) ) ;
968+
969+ let request = UpsertAgentRequest {
970+ enabled : Some ( false ) ,
971+ sandbox_enabled : Some ( true ) ,
972+ ai_provider : None ,
973+ api_key : None ,
974+ ai_provider_key_id : None ,
975+ daily_budget_cents : None ,
976+ max_turns : None ,
977+ cooldown_minutes : None ,
978+ trigger_config : None ,
979+ prompt : None ,
980+ timeout_seconds : None ,
981+ deliverable : None ,
982+ slug : None ,
983+ name : None ,
984+ description : None ,
985+ branch_prefix : None ,
986+ } ;
987+
988+ let result = svc. upsert_config ( 1 , request) . await ;
989+ assert ! (
990+ result. is_ok( ) ,
991+ "upsert with sandbox_enabled=Some(true) should succeed"
992+ ) ;
993+ let model = result. unwrap ( ) ;
994+ assert_eq ! (
995+ model. sandbox_enabled,
996+ Some ( true ) ,
997+ "sandbox_enabled should be Some(true) in the returned model"
998+ ) ;
999+ }
1000+
1001+ #[ tokio:: test]
1002+ async fn test_upsert_sandbox_enabled_some_false_stored_correctly ( ) {
1003+ let mut expected = make_config ( 1 ) ;
1004+ expected. sandbox_enabled = Some ( false ) ;
1005+
1006+ let db = MockDatabase :: new ( DatabaseBackend :: Postgres )
1007+ . append_query_results ( vec ! [ Vec :: <project_agents:: Model >:: new( ) ] ) // no existing config
1008+ . append_query_results ( vec ! [ vec![ expected. clone( ) ] ] ) // insert returns model
1009+ . into_connection ( ) ;
1010+ let svc = AgentConfigService :: new ( Arc :: new ( db) , make_encryption_service ( ) ) ;
1011+
1012+ let request = UpsertAgentRequest {
1013+ enabled : Some ( false ) ,
1014+ sandbox_enabled : Some ( false ) ,
1015+ ai_provider : None ,
1016+ api_key : None ,
1017+ ai_provider_key_id : None ,
1018+ daily_budget_cents : None ,
1019+ max_turns : None ,
1020+ cooldown_minutes : None ,
1021+ trigger_config : None ,
1022+ prompt : None ,
1023+ timeout_seconds : None ,
1024+ deliverable : None ,
1025+ slug : None ,
1026+ name : None ,
1027+ description : None ,
1028+ branch_prefix : None ,
1029+ } ;
1030+
1031+ let result = svc. upsert_config ( 1 , request) . await ;
1032+ assert ! (
1033+ result. is_ok( ) ,
1034+ "upsert with sandbox_enabled=Some(false) should succeed"
1035+ ) ;
1036+ let model = result. unwrap ( ) ;
1037+ assert_eq ! (
1038+ model. sandbox_enabled,
1039+ Some ( false ) ,
1040+ "sandbox_enabled should be Some(false) in the returned model"
1041+ ) ;
1042+ }
1043+
1044+ /// Verify the sandbox override logic in isolation:
1045+ /// `config.sandbox_enabled.unwrap_or(global_sandbox.enabled)`
1046+ #[ test]
1047+ fn test_sandbox_enabled_option_logic_all_combinations ( ) {
1048+ fn resolve ( agent : Option < bool > , global : bool ) -> bool {
1049+ agent. unwrap_or ( global)
1050+ }
1051+ // None + global=false → false
1052+ assert ! ( !resolve( None , false ) ) ;
1053+ // None + global=true → true
1054+ assert ! ( resolve( None , true ) ) ;
1055+ // Some(true) + global=false → true (per-agent overrides global)
1056+ assert ! ( resolve( Some ( true ) , false ) ) ;
1057+ // Some(true) + global=true → true
1058+ assert ! ( resolve( Some ( true ) , true ) ) ;
1059+ // Some(false) + global=false → false
1060+ assert ! ( !resolve( Some ( false ) , false ) ) ;
1061+ // Some(false) + global=true → false (per-agent overrides global)
1062+ assert ! ( !resolve( Some ( false ) , true ) ) ;
1063+ }
9101064}
0 commit comments