@@ -363,10 +363,7 @@ async fn collect_stats(sessions_dir: &PathBuf, cli: &StatsCli) -> Result<UsageSt
363363 // Try to parse the session
364364 if let Ok ( session_data) = parse_session_file ( & path) {
365365 // Check if session is within date range
366- if let Some ( ref timestamp) = session_data. timestamp
367- && let Ok ( session_date) = chrono:: DateTime :: parse_from_rfc3339 ( timestamp)
368- && session_date < start_date
369- {
366+ if !session_is_within_date_range ( session_data. timestamp . as_deref ( ) , & start_date) {
370367 continue ;
371368 }
372369
@@ -430,6 +427,21 @@ async fn collect_stats(sessions_dir: &PathBuf, cli: &StatsCli) -> Result<UsageSt
430427 Ok ( stats)
431428}
432429
430+ fn session_is_within_date_range (
431+ timestamp : Option < & str > ,
432+ start_date : & chrono:: DateTime < chrono:: Utc > ,
433+ ) -> bool {
434+ let Some ( timestamp) = timestamp else {
435+ return false ;
436+ } ;
437+
438+ let Ok ( session_date) = chrono:: DateTime :: parse_from_rfc3339 ( timestamp) else {
439+ return false ;
440+ } ;
441+
442+ session_date. with_timezone ( & chrono:: Utc ) >= * start_date
443+ }
444+
433445/// Session data extracted from file.
434446#[ derive( Debug , Default ) ]
435447struct SessionData {
@@ -735,6 +747,77 @@ mod tests {
735747 assert ! ( ( cost - 12.5 ) . abs( ) < 0.001 ) ;
736748 }
737749
750+ #[ test]
751+ fn test_session_date_range_requires_valid_timestamp ( ) {
752+ let start_date = chrono:: Utc :: now ( ) - chrono:: Duration :: days ( 1 ) ;
753+ let recent = chrono:: Utc :: now ( ) . to_rfc3339 ( ) ;
754+ let old = ( chrono:: Utc :: now ( ) - chrono:: Duration :: days ( 2 ) ) . to_rfc3339 ( ) ;
755+
756+ assert ! ( session_is_within_date_range( Some ( & recent) , & start_date) ) ;
757+ assert ! ( !session_is_within_date_range( Some ( & old) , & start_date) ) ;
758+ assert ! ( !session_is_within_date_range( None , & start_date) ) ;
759+ assert ! ( !session_is_within_date_range(
760+ Some ( "not-a-date" ) ,
761+ & start_date
762+ ) ) ;
763+ }
764+
765+ #[ tokio:: test]
766+ async fn test_collect_stats_excludes_missing_and_invalid_timestamps ( ) {
767+ let temp_dir = tempfile:: tempdir ( ) . unwrap ( ) ;
768+ let sessions_dir = temp_dir. path ( ) . to_path_buf ( ) ;
769+ let recent = chrono:: Utc :: now ( ) . to_rfc3339 ( ) ;
770+
771+ std:: fs:: write (
772+ sessions_dir. join ( "recent.json" ) ,
773+ format ! (
774+ r#"{{
775+ "created_at": "{recent}",
776+ "model": "gpt-4o",
777+ "messages": [{{"role": "user", "content": "valid"}}],
778+ "usage": {{"input_tokens": 100, "output_tokens": 100}}
779+ }}"#
780+ ) ,
781+ )
782+ . unwrap ( ) ;
783+
784+ std:: fs:: write (
785+ sessions_dir. join ( "missing.json" ) ,
786+ r#"{
787+ "model": "gpt-4o",
788+ "messages": [{"role": "user", "content": "missing timestamp"}],
789+ "usage": {"input_tokens": 9999, "output_tokens": 9999}
790+ }"# ,
791+ )
792+ . unwrap ( ) ;
793+
794+ std:: fs:: write (
795+ sessions_dir. join ( "invalid.json" ) ,
796+ r#"{
797+ "timestamp": "not-a-date",
798+ "model": "gpt-4o",
799+ "messages": [{"role": "user", "content": "invalid timestamp"}],
800+ "usage": {"input_tokens": 8888, "output_tokens": 8888}
801+ }"# ,
802+ )
803+ . unwrap ( ) ;
804+
805+ let cli = StatsCli {
806+ days : 1 ,
807+ provider : None ,
808+ model : None ,
809+ json : false ,
810+ verbose : false ,
811+ } ;
812+
813+ let stats = collect_stats ( & sessions_dir, & cli) . await . unwrap ( ) ;
814+
815+ assert_eq ! ( stats. total_sessions, 1 ) ;
816+ assert_eq ! ( stats. total_messages, 1 ) ;
817+ assert_eq ! ( stats. input_tokens, 100 ) ;
818+ assert_eq ! ( stats. output_tokens, 100 ) ;
819+ }
820+
738821 #[ test]
739822 fn test_validate_days_range ( ) {
740823 // Valid values
0 commit comments