@@ -668,7 +668,7 @@ impl LocalAiService {
668668 async fn list_models ( & self ) -> Result < Vec < OllamaModelTag > , String > {
669669 let response = self
670670 . http
671- . get ( format ! ( "{OLLAMA_BASE_URL }/api/tags" ) )
671+ . get ( format ! ( "{}/api/tags" , ollama_base_url ( ) ) )
672672 . timeout ( std:: time:: Duration :: from_secs ( 5 ) )
673673 . send ( )
674674 . await
@@ -936,4 +936,147 @@ mod tests {
936936 std:: env:: remove_var ( "OPENHUMAN_OLLAMA_BASE_URL" ) ;
937937 }
938938 }
939+
940+ #[ tokio:: test]
941+ async fn diagnostics_reports_server_unreachable_when_url_unbound ( ) {
942+ let _guard = crate :: openhuman:: local_ai:: LOCAL_AI_TEST_MUTEX
943+ . lock ( )
944+ . expect ( "local ai mutex" ) ;
945+
946+ unsafe {
947+ std:: env:: set_var ( "OPENHUMAN_OLLAMA_BASE_URL" , "http://127.0.0.1:1" ) ;
948+ }
949+ let config = Config :: default ( ) ;
950+ let service = LocalAiService :: new ( & config) ;
951+ let diag = service. diagnostics ( & config) . await . expect ( "diagnostics" ) ;
952+ assert_eq ! ( diag[ "ollama_running" ] , false ) ;
953+ let issues = diag[ "issues" ] . as_array ( ) . cloned ( ) . unwrap_or_default ( ) ;
954+ assert ! (
955+ !issues. is_empty( ) ,
956+ "unreachable server must surface an issue"
957+ ) ;
958+ assert ! ( issues
959+ . iter( )
960+ . any( |v| v. as_str( ) . unwrap_or( "" ) . contains( "not running" ) ) ) ;
961+ unsafe {
962+ std:: env:: remove_var ( "OPENHUMAN_OLLAMA_BASE_URL" ) ;
963+ }
964+ }
965+
966+ #[ tokio:: test]
967+ async fn diagnostics_with_running_server_but_missing_models_flags_issues ( ) {
968+ let _guard = crate :: openhuman:: local_ai:: LOCAL_AI_TEST_MUTEX
969+ . lock ( )
970+ . expect ( "local ai mutex" ) ;
971+
972+ let app = Router :: new ( ) . route ( "/api/tags" , get ( || async { Json ( json ! ( { "models" : [ ] } ) ) } ) ) ;
973+ let base = spawn_mock ( app) . await ;
974+ unsafe {
975+ std:: env:: set_var ( "OPENHUMAN_OLLAMA_BASE_URL" , & base) ;
976+ }
977+
978+ let config = Config :: default ( ) ;
979+ let service = LocalAiService :: new ( & config) ;
980+ let diag = service. diagnostics ( & config) . await . expect ( "diagnostics" ) ;
981+ assert_eq ! ( diag[ "ollama_running" ] , true ) ;
982+ // No models are installed → expected chat model issue surfaces.
983+ let issues = diag[ "issues" ] . as_array ( ) . cloned ( ) . unwrap_or_default ( ) ;
984+ assert ! ( !issues. is_empty( ) ) ;
985+ unsafe {
986+ std:: env:: remove_var ( "OPENHUMAN_OLLAMA_BASE_URL" ) ;
987+ }
988+ }
989+
990+ #[ tokio:: test]
991+ async fn diagnostics_ok_when_expected_models_are_present ( ) {
992+ let _guard = crate :: openhuman:: local_ai:: LOCAL_AI_TEST_MUTEX
993+ . lock ( )
994+ . expect ( "local ai mutex" ) ;
995+
996+ let config = Config :: default ( ) ;
997+ let chat = crate :: openhuman:: local_ai:: model_ids:: effective_chat_model_id ( & config) ;
998+ let chat_tag = format ! ( "{}:latest" , chat) ;
999+ let app = Router :: new ( ) . route (
1000+ "/api/tags" ,
1001+ get ( move || {
1002+ let chat_tag = chat_tag. clone ( ) ;
1003+ async move {
1004+ Json ( json ! ( {
1005+ "models" : [
1006+ { "name" : chat_tag, "modified_at" : "" , "size" : 1u64 , "digest" : "d" }
1007+ ]
1008+ } ) )
1009+ }
1010+ } ) ,
1011+ ) ;
1012+ let base = spawn_mock ( app) . await ;
1013+ unsafe {
1014+ std:: env:: set_var ( "OPENHUMAN_OLLAMA_BASE_URL" , & base) ;
1015+ }
1016+
1017+ let service = LocalAiService :: new ( & config) ;
1018+ let diag = service. diagnostics ( & config) . await . expect ( "diagnostics" ) ;
1019+ assert_eq ! ( diag[ "ollama_running" ] , true ) ;
1020+ assert_eq ! ( diag[ "expected" ] [ "chat_found" ] , true ) ;
1021+ unsafe {
1022+ std:: env:: remove_var ( "OPENHUMAN_OLLAMA_BASE_URL" ) ;
1023+ }
1024+ }
1025+
1026+ #[ tokio:: test]
1027+ async fn list_models_returns_parsed_payload ( ) {
1028+ let _guard = crate :: openhuman:: local_ai:: LOCAL_AI_TEST_MUTEX
1029+ . lock ( )
1030+ . expect ( "local ai mutex" ) ;
1031+
1032+ let app = Router :: new ( ) . route (
1033+ "/api/tags" ,
1034+ get ( || async {
1035+ Json ( json ! ( {
1036+ "models" : [
1037+ { "name" : "a:latest" , "modified_at" : "t" , "size" : 1u64 , "digest" : "d1" } ,
1038+ { "name" : "b:v2" , "modified_at" : "t" , "size" : 2u64 , "digest" : "d2" }
1039+ ]
1040+ } ) )
1041+ } ) ,
1042+ ) ;
1043+ let base = spawn_mock ( app) . await ;
1044+ unsafe {
1045+ std:: env:: set_var ( "OPENHUMAN_OLLAMA_BASE_URL" , & base) ;
1046+ }
1047+
1048+ let config = Config :: default ( ) ;
1049+ let service = LocalAiService :: new ( & config) ;
1050+ let models = service. list_models ( ) . await . expect ( "list_models" ) ;
1051+ assert_eq ! ( models. len( ) , 2 ) ;
1052+ assert_eq ! ( models[ 0 ] . name, "a:latest" ) ;
1053+ assert_eq ! ( models[ 1 ] . name, "b:v2" ) ;
1054+ unsafe {
1055+ std:: env:: remove_var ( "OPENHUMAN_OLLAMA_BASE_URL" ) ;
1056+ }
1057+ }
1058+
1059+ #[ tokio:: test]
1060+ async fn list_models_errors_on_non_success ( ) {
1061+ let _guard = crate :: openhuman:: local_ai:: LOCAL_AI_TEST_MUTEX
1062+ . lock ( )
1063+ . expect ( "local ai mutex" ) ;
1064+
1065+ let app = Router :: new ( ) . route (
1066+ "/api/tags" ,
1067+ get ( || async { ( axum:: http:: StatusCode :: SERVICE_UNAVAILABLE , "down" ) } ) ,
1068+ ) ;
1069+ let base = spawn_mock ( app) . await ;
1070+ unsafe {
1071+ std:: env:: set_var ( "OPENHUMAN_OLLAMA_BASE_URL" , & base) ;
1072+ }
1073+
1074+ let config = Config :: default ( ) ;
1075+ let service = LocalAiService :: new ( & config) ;
1076+ let err = service. list_models ( ) . await . unwrap_err ( ) ;
1077+ assert ! ( err. contains( "503" ) || err. contains( "tags failed" ) ) ;
1078+ unsafe {
1079+ std:: env:: remove_var ( "OPENHUMAN_OLLAMA_BASE_URL" ) ;
1080+ }
1081+ }
9391082}
0 commit comments