@@ -551,6 +551,26 @@ struct FileSignature {
551551 local_ordinal : u32 ,
552552}
553553
554+ impl FileSignature {
555+ fn new ( local_now : chrono:: DateTime < Local > ) -> Self {
556+ Self {
557+ files : 0 ,
558+ bytes : 0 ,
559+ newest_mtime_ns : 0 ,
560+ local_year : local_now. year ( ) ,
561+ local_ordinal : local_now. ordinal ( ) ,
562+ }
563+ }
564+
565+ fn observe_file ( & mut self , len : u64 , modified : std:: time:: SystemTime ) {
566+ if let Ok ( delta) = modified. duration_since ( std:: time:: UNIX_EPOCH ) {
567+ self . newest_mtime_ns = self . newest_mtime_ns . max ( delta. as_nanos ( ) ) ;
568+ }
569+ self . files += 1 ;
570+ self . bytes = self . bytes . saturating_add ( len) ;
571+ }
572+ }
573+
554574#[ derive( Debug , Default , Clone ) ]
555575struct LocalTokenStats {
556576 today : CodexPeriodStats ,
@@ -630,13 +650,7 @@ pub fn collect() -> Result<CodexStats> {
630650 }
631651
632652 let mut jsonl_files: Vec < ( PathBuf , std:: fs:: Metadata ) > = Vec :: new ( ) ;
633- let mut signature = FileSignature {
634- files : 0 ,
635- bytes : 0 ,
636- newest_mtime_ns : 0 ,
637- local_year : now_local. year ( ) ,
638- local_ordinal : now_local. ordinal ( ) ,
639- } ;
653+ let mut signature = FileSignature :: new ( now_local) ;
640654 for entry in roots
641655 . iter ( )
642656 . flat_map ( |r| WalkDir :: new ( r) . into_iter ( ) )
@@ -655,12 +669,10 @@ pub fn collect() -> Result<CodexStats> {
655669 if mt < cutoff_30d {
656670 continue ;
657671 }
658- if let Ok ( delta ) = mtime . duration_since ( std :: time :: UNIX_EPOCH ) {
659- signature . newest_mtime_ns = signature . newest_mtime_ns . max ( delta . as_nanos ( ) ) ;
660- }
672+ signature . observe_file ( meta . len ( ) , mtime ) ;
673+ } else {
674+ signature . observe_file ( meta . len ( ) , std :: time :: UNIX_EPOCH ) ;
661675 }
662- signature. files += 1 ;
663- signature. bytes = signature. bytes . saturating_add ( meta. len ( ) ) ;
664676 jsonl_files. push ( ( path, meta) ) ;
665677 }
666678
@@ -795,3 +807,56 @@ fn apply_local_token_stats(stats: &mut CodexStats, local: &LocalTokenStats) {
795807 stats. d30 = local. d30 . clone ( ) ;
796808 stats. mtd = local. mtd . clone ( ) ;
797809}
810+
811+ #[ cfg( test) ]
812+ mod local_cache_tests {
813+ use super :: * ;
814+ use chrono:: TimeZone ;
815+
816+ #[ test]
817+ fn file_signature_changes_on_file_or_day_changes ( ) {
818+ let day = Local . with_ymd_and_hms ( 2026 , 5 , 27 , 8 , 0 , 0 ) . unwrap ( ) ;
819+ let next_day = Local . with_ymd_and_hms ( 2026 , 5 , 28 , 8 , 0 , 0 ) . unwrap ( ) ;
820+ let mtime = std:: time:: UNIX_EPOCH + std:: time:: Duration :: from_secs ( 10 ) ;
821+
822+ let mut base = FileSignature :: new ( day) ;
823+ base. observe_file ( 42 , mtime) ;
824+
825+ let mut same = FileSignature :: new ( day) ;
826+ same. observe_file ( 42 , mtime) ;
827+ assert_eq ! ( base, same) ;
828+
829+ let mut size_changed = FileSignature :: new ( day) ;
830+ size_changed. observe_file ( 43 , mtime) ;
831+ assert_ne ! ( base, size_changed) ;
832+
833+ let mut day_changed = FileSignature :: new ( next_day) ;
834+ day_changed. observe_file ( 42 , mtime) ;
835+ assert_ne ! ( base, day_changed) ;
836+ }
837+
838+ #[ test]
839+ fn cached_local_token_stats_do_not_overwrite_live_limit_fields ( ) {
840+ let mut stats = CodexStats {
841+ plan_label : "PRO 5x" . to_string ( ) ,
842+ plan_label_raw : "prolite" . to_string ( ) ,
843+ last_event_at : Some ( Utc :: now ( ) ) ,
844+ ..Default :: default ( )
845+ } ;
846+ stats. rate_limits . primary_used_percent = 12.0 ;
847+ let local = LocalTokenStats {
848+ today : CodexPeriodStats {
849+ requests : 7 ,
850+ ..Default :: default ( )
851+ } ,
852+ ..Default :: default ( )
853+ } ;
854+
855+ apply_local_token_stats ( & mut stats, & local) ;
856+
857+ assert_eq ! ( stats. today. requests, 7 ) ;
858+ assert_eq ! ( stats. rate_limits. primary_used_percent, 12.0 ) ;
859+ assert_eq ! ( stats. plan_label_raw, "prolite" ) ;
860+ assert ! ( stats. last_event_at. is_some( ) ) ;
861+ }
862+ }
0 commit comments