@@ -1411,12 +1411,44 @@ fn git_usage_cache_is_stale(
14111411 return true ;
14121412 }
14131413
1414+ if !git_usage_cache_has_commit_details ( cache) {
1415+ return true ;
1416+ }
1417+
14141418 if max_age_minutes <= 0 {
14151419 return true ;
14161420 }
14171421 ( Utc :: now ( ) - cache. generated_at ) . num_minutes ( ) >= max_age_minutes
14181422}
14191423
1424+ fn git_usage_cache_has_commit_details ( cache : & git_usage:: GitUsageCache ) -> bool {
1425+ [
1426+ & cache. today ,
1427+ & cache. last3_days ,
1428+ & cache. this_week ,
1429+ & cache. this_month ,
1430+ ]
1431+ . into_iter ( )
1432+ . all ( git_usage_report_has_commit_details)
1433+ && cache
1434+ . custom_days
1435+ . iter ( )
1436+ . all ( git_usage_cached_day_has_commit_details)
1437+ }
1438+
1439+ fn git_usage_report_has_commit_details ( report : & GitUsageReport ) -> bool {
1440+ let has_activity = report. totals . added_lines > 0
1441+ || report. totals . deleted_lines > 0
1442+ || report. totals . changed_files > 0 ;
1443+ !has_activity || !report. commits . is_empty ( )
1444+ }
1445+
1446+ fn git_usage_cached_day_has_commit_details ( day : & git_usage:: GitUsageCachedDay ) -> bool {
1447+ let has_activity =
1448+ day. totals . added_lines > 0 || day. totals . deleted_lines > 0 || day. totals . changed_files > 0 ;
1449+ !has_activity || !day. commits . is_empty ( )
1450+ }
1451+
14201452fn pr_kpi_cache_is_stale (
14211453 cache : & pr_kpi:: PrKpiCache ,
14221454 max_age_minutes : i64 ,
@@ -1870,6 +1902,43 @@ mod tests {
18701902 assert ! ( !git_usage_cache_is_stale( & cache, 15 , "/tmp/old" ) ) ;
18711903 }
18721904
1905+ #[ test]
1906+ fn git_usage_cache_is_stale_when_commit_details_are_missing_from_active_cache ( ) {
1907+ let now = chrono:: Utc :: now ( ) ;
1908+ let mut cache = git_usage:: GitUsageCache {
1909+ root_path : "/tmp/old" . into ( ) ,
1910+ generated_at : now,
1911+ today : git_usage:: empty_report ( LocalTokenUsageRange :: Today , None ) ,
1912+ last3_days : git_usage:: empty_report ( LocalTokenUsageRange :: Last3Days , None ) ,
1913+ this_week : git_usage:: empty_report ( LocalTokenUsageRange :: ThisWeek , None ) ,
1914+ this_month : git_usage:: empty_report ( LocalTokenUsageRange :: ThisMonth , None ) ,
1915+ custom_window_start : None ,
1916+ custom_window_end : None ,
1917+ custom_days : vec ! [ ] ,
1918+ } ;
1919+ cache. today . totals . added_lines = 12 ;
1920+ cache. today . totals . deleted_lines = 3 ;
1921+ cache. today . commits . clear ( ) ;
1922+
1923+ assert ! ( git_usage_cache_is_stale( & cache, 15 , "/tmp/old" ) ) ;
1924+
1925+ cache. today . commits . push ( crate :: models:: GitUsageCommit {
1926+ commit_hash : "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" . into ( ) ,
1927+ short_hash : "aaaaaaaaaa" . into ( ) ,
1928+ timestamp : now,
1929+ author_name : "Test User" . into ( ) ,
1930+ author_email : "test@example.com" . into ( ) ,
1931+ subject : "test commit" . into ( ) ,
1932+ repository_name : "repo" . into ( ) ,
1933+ repository_path : "/tmp/repo" . into ( ) ,
1934+ added_lines : 12 ,
1935+ deleted_lines : 3 ,
1936+ changed_files : 1 ,
1937+ } ) ;
1938+
1939+ assert ! ( !git_usage_cache_is_stale( & cache, 15 , "/tmp/old" ) ) ;
1940+ }
1941+
18731942 #[ test]
18741943 fn custom_usage_range_request_rejects_invalid_dates ( ) {
18751944 let today = chrono:: NaiveDate :: from_ymd_opt ( 2026 , 4 , 27 ) . unwrap ( ) ;
0 commit comments