@@ -1039,72 +1039,130 @@ mod tests {
10391039 backend. join_from_url ( join_url. as_str ( ) ) . await ?;
10401040 }
10411041
1042- // Wait for replication; then retry refresh until P2P converges (same pattern as save-dweb-backend).
1043- // Propagation can be slow in CI/P2P environments.
1044- tokio:: time:: sleep ( Duration :: from_secs ( 10 ) ) . await ;
1045-
1046- // Retry refresh until P2P replication converges: the refresh must succeed AND
1047- // the response must contain the creator's repo (by name) with the uploaded file.
1048- // Repo names and file lists propagate via DHT and may lag behind the initial join.
1049- let mut refresh_retries = 12 ;
1042+ // Wait for replication and then run convergence in two phases:
1043+ // A) metadata convergence (group + repo visible on joiner)
1044+ // B) content convergence (file downloadable on joiner)
1045+ // This reduces flakiness caused by asking refresh to do all work while relays are unstable.
1046+ tokio:: time:: sleep ( Duration :: from_secs ( 6 ) ) . await ;
1047+
1048+ {
1049+ let mut retries = 20 ;
1050+ loop {
1051+ let groups_req = test:: TestRequest :: get ( ) . uri ( "/api/groups" ) . to_request ( ) ;
1052+ let groups_resp: GroupsResponse =
1053+ test:: call_and_read_body_json ( & app, groups_req) . await ;
1054+ let repos_req = test:: TestRequest :: get ( )
1055+ . uri ( & format ! ( "/api/groups/{}/repos" , group. id( ) ) )
1056+ . to_request ( ) ;
1057+ let repos_resp: ReposResponse =
1058+ test:: call_and_read_body_json ( & app, repos_req) . await ;
1059+
1060+ let has_group = groups_resp
1061+ . groups
1062+ . iter ( )
1063+ . any ( |g| g. key == group. id ( ) . to_string ( ) ) ;
1064+ let has_any_repo = !repos_resp. repos . is_empty ( ) ;
1065+ let ok = has_group && has_any_repo;
1066+
1067+ if ok {
1068+ break ;
1069+ }
1070+
1071+ retries -= 1 ;
1072+ if retries == 0 {
1073+ panic ! (
1074+ "Refresh-joined metadata did not converge after retries. groups: {}, repos: {}" ,
1075+ groups_resp. groups. len( ) ,
1076+ repos_resp. repos. len( ) ,
1077+ ) ;
1078+ }
1079+ tokio:: time:: sleep ( Duration :: from_secs ( 5 ) ) . await ;
1080+ }
1081+ } ;
1082+
1083+ // Retry refresh until it returns the expected repo + file view.
1084+ let mut refresh_retries = 20 ;
10501085 let refresh_data: serde_json:: Value = loop {
10511086 let refresh_req = test:: TestRequest :: post ( )
10521087 . uri ( & format ! ( "/api/groups/{}/refresh" , group. id( ) ) )
10531088 . to_request ( ) ;
10541089 let resp = test:: call_service ( & app, refresh_req) . await ;
1090+ let resp_status = resp. status ( ) ;
10551091
1056- if resp . status ( ) . is_success ( ) {
1092+ if resp_status . is_success ( ) {
10571093 let data: serde_json:: Value = test:: read_body_json ( resp) . await ;
1058- // Check if the expected repo with the correct name and file is present
1059- let has_expected_repo = data[ "repos" ] . as_array ( )
1060- . and_then ( |repos| repos. iter ( ) . find ( |r| r[ "name" ] == TEST_GROUP_NAME ) )
1061- . and_then ( |repo| repo[ "all_files" ] . as_array ( ) )
1062- . map ( |files| files. iter ( ) . any ( |f| f. as_str ( ) == Some ( file_name) ) )
1094+ let has_repo_with_file = data[ "repos" ]
1095+ . as_array ( )
1096+ . map ( |repos| {
1097+ repos. iter ( ) . any ( |repo| {
1098+ repo[ "all_files" ]
1099+ . as_array ( )
1100+ . map ( |files| files. iter ( ) . any ( |f| f. as_str ( ) == Some ( file_name) ) )
1101+ . unwrap_or ( false )
1102+ } )
1103+ } )
10631104 . unwrap_or ( false ) ;
10641105
1065- if has_expected_repo {
1106+ if has_repo_with_file {
10661107 break data;
10671108 }
1068- log:: warn!( "Refresh succeeded but repo data not yet propagated (attempt {})" ,
1069- 12 - refresh_retries + 1 ) ;
1109+ log:: warn!(
1110+ "Refresh succeeded but expected repo/file not yet visible (attempt {})" ,
1111+ 21 - refresh_retries
1112+ ) ;
10701113 } else {
1071- log:: warn!( "Refresh failed (attempt {}): status={}, body={:?}" ,
1072- 12 - refresh_retries + 1 ,
1073- resp. status( ) ,
1074- test:: read_body( resp) . await
1114+ let body = test:: read_body ( resp) . await ;
1115+ let body_text = String :: from_utf8_lossy ( & body) . to_string ( ) ;
1116+ let is_retryable = body_text. contains ( "couldn't look up relay" )
1117+ || body_text. contains ( "Unable to open group DHT record" )
1118+ || body_text. contains ( "Group not found:" ) ;
1119+ log:: warn!(
1120+ "Refresh failed (attempt {}): status={}, body={}" ,
1121+ 21 - refresh_retries,
1122+ resp_status,
1123+ body_text
10751124 ) ;
1125+ if !is_retryable {
1126+ panic ! (
1127+ "Refresh failed with non-retryable error. status={resp_status}, body={body_text}"
1128+ ) ;
1129+ }
10761130 }
10771131
10781132 refresh_retries -= 1 ;
10791133 if refresh_retries == 0 {
1080- panic ! ( "Refresh did not converge after 12 attempts." ) ;
1134+ panic ! ( "Refresh did not converge after 20 attempts." ) ;
10811135 }
1082- tokio:: time:: sleep ( Duration :: from_secs ( 4 ) ) . await ;
1136+ tokio:: time:: sleep ( Duration :: from_secs ( 5 ) ) . await ;
10831137 } ;
10841138
10851139 assert_eq ! ( refresh_data[ "status" ] , "success" , "First refresh status should be success" ) ;
10861140
10871141 let repos = refresh_data[ "repos" ] . as_array ( ) . expect ( "repos should be an array" ) ;
10881142 assert ! ( !repos. is_empty( ) , "Should have at least one repo after joining" ) ;
10891143
1090- let repo_data = repos. iter ( )
1091- . find ( |r| r[ "name" ] == TEST_GROUP_NAME )
1092- . expect ( "Should find the creator's repo by name" ) ;
1144+ let repo_data = repos
1145+ . iter ( )
1146+ . find ( |r| {
1147+ r[ "all_files" ]
1148+ . as_array ( )
1149+ . map ( |files| files. iter ( ) . any ( |f| f. as_str ( ) == Some ( file_name) ) )
1150+ . unwrap_or ( false )
1151+ } )
1152+ . expect ( "Should find a repo containing the uploaded file" ) ;
1153+ let refreshed_repo_id = repo_data[ "repo_id" ]
1154+ . as_str ( )
1155+ . expect ( "repo_id should be a string" )
1156+ . to_string ( ) ;
10931157
10941158 let refreshed_files = repo_data[ "refreshed_files" ] . as_array ( )
10951159 . expect ( "refreshed_files should be an array" ) ;
10961160 assert ! (
1097- refreshed_files. len( ) <= 1 ,
1098- "First refresh should refresh at most one file, got {}" ,
1099- refreshed_files. len( )
1161+ refreshed_files. is_empty( )
1162+ || ( refreshed_files. len( ) == 1
1163+ && refreshed_files[ 0 ] . as_str( ) == Some ( file_name) ) ,
1164+ "First refresh should report either no-op or one refreshed expected file, got {refreshed_files:?}"
11001165 ) ;
1101- if refreshed_files. len ( ) == 1 {
1102- assert_eq ! (
1103- refreshed_files[ 0 ] . as_str( ) . unwrap( ) ,
1104- file_name,
1105- "Should have refreshed the correct file"
1106- ) ;
1107- }
11081166
11091167 let all_files = repo_data[ "all_files" ] . as_array ( ) . expect ( "all_files should be an array" ) ;
11101168 assert_eq ! ( all_files. len( ) , 1 , "Should have one file in all_files" ) ;
@@ -1115,7 +1173,7 @@ mod tests {
11151173 let get_file_req = test:: TestRequest :: get ( )
11161174 . uri ( & format ! (
11171175 "/api/groups/{}/repos/{}/media/{}" ,
1118- group. id( ) , repo . id ( ) , file_name
1176+ group. id( ) , refreshed_repo_id , file_name
11191177 ) )
11201178 . to_request ( ) ;
11211179 let get_file_resp = test:: call_service ( & app, get_file_req) . await ;
@@ -1137,9 +1195,15 @@ mod tests {
11371195 let repos2 = refresh_data2[ "repos" ] . as_array ( ) . expect ( "repos should be an array" ) ;
11381196 assert ! ( !repos2. is_empty( ) , "Should still have repos" ) ;
11391197
1140- let repo_data2 = repos2. iter ( )
1141- . find ( |r| r[ "name" ] == TEST_GROUP_NAME )
1142- . expect ( "Should still find the creator's repo by name on second refresh" ) ;
1198+ let repo_data2 = repos2
1199+ . iter ( )
1200+ . find ( |r| {
1201+ r[ "all_files" ]
1202+ . as_array ( )
1203+ . map ( |files| files. iter ( ) . any ( |f| f. as_str ( ) == Some ( file_name) ) )
1204+ . unwrap_or ( false )
1205+ } )
1206+ . expect ( "Should still find the repo containing the uploaded file on second refresh" ) ;
11431207 let refreshed_files2 = repo_data2[ "refreshed_files" ] . as_array ( )
11441208 . expect ( "refreshed_files should be an array" ) ;
11451209 assert ! ( refreshed_files2. is_empty( ) ,
0 commit comments