From e5f1d9f42fff6c0f0412fcfd71d6b092489a45f5 Mon Sep 17 00:00:00 2001 From: Amy OpenClaw Date: Tue, 14 Jul 2026 08:17:38 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20Filter=20out-of-month=20work?= =?UTF-8?q?days=20from=20monthly=20work=20stats?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/gdarquie_work/work.rs | 104 ++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/src/plugins/gdarquie_work/work.rs b/src/plugins/gdarquie_work/work.rs index bd971bd..0cbd893 100644 --- a/src/plugins/gdarquie_work/work.rs +++ b/src/plugins/gdarquie_work/work.rs @@ -105,6 +105,17 @@ pub fn compute_monthly_work_stats(month: Option<&str>) -> Result = HashMap::new(); let mut total_duration = 0; @@ -356,6 +367,99 @@ mod tests { assert!(length_in_minutes.contains(&120)); } + // Helper that mimics compute_monthly_work_stats's month-filtering logic. + // Takes a target month (year, month) and a flat list of annotations with explicit workdays. + fn compute_monthly_stats_with_workday( + year: i32, + month: u32, + pairs: Vec<(chrono::DateTime, chrono::DateTime, &str)>, + ) -> MonthlyWorkStats { + let target_date = + chrono::NaiveDate::from_ymd_opt(year, month, 1).unwrap(); + + let mut annotations_hmap: HashMap> = HashMap::new(); + for (start_dt, stop_dt, workday) in pairs { + let mut a_start = make_annotation(EventName::StartWork, start_dt); + a_start.workday = Some(workday.to_string()); + let mut a_stop = make_annotation(EventName::StopWork, stop_dt); + a_stop.workday = Some(workday.to_string()); + annotations_hmap + .entry(workday.to_string()) + .or_default() + .push(a_start); + annotations_hmap + .entry(workday.to_string()) + .or_default() + .push(a_stop); + } + + // apply the month filter (the fix under test) + annotations_hmap.retain(|workday, _| { + chrono::NaiveDate::parse_from_str(workday, "%Y-%m-%d") + .map(|d| d.year() == target_date.year() && d.month() == target_date.month()) + .unwrap_or(false) + }); + + let mut work_stats_by_week: HashMap = HashMap::new(); + let mut total_duration = 0; + let mut worked_days_set = HashSet::new(); + + for (day, anns) in &annotations_hmap { + let length = compute_work_time_from_annotations(anns); + let date = chrono::NaiveDate::parse_from_str(day, "%Y-%m-%d").unwrap(); + let week_id = WeekId { + year: date.iso_week().year(), + week: date.iso_week().week(), + }; + let entry = work_stats_by_week.entry(week_id).or_insert(WorkStatsByWeek { + total_duration_in_minutes: 0, + work_stats: vec![], + }); + entry.total_duration_in_minutes += length; + entry.work_stats.push(WorkStats { + day: day.clone(), + length_in_minutes: length, + }); + total_duration += length; + worked_days_set.insert(day.clone()); + } + + MonthlyWorkStats { + total_duration_in_minutes: total_duration, + total_work_days: worked_days_set.len() as i32, + work_stats_by_week, + } + } + + /// Regression test: when end_work crosses midnight from the last day of the + /// previous month into the first day of the current month, phantom annotations + /// tagged with the previous month's workday must NOT inflate total_work_days. + #[test] + fn test_work_stats_no_extra_day_when_session_crosses_month_boundary() { + let tz = FixedOffset::east_opt(0).unwrap(); + + // Real session on Sep 1 + let sep1_start = tz.with_ymd_and_hms(2025, 9, 1, 9, 0, 0).unwrap(); + let sep1_stop = tz.with_ymd_and_hms(2025, 9, 1, 17, 0, 0).unwrap(); + + // Phantom session: end_work crossed midnight from Aug 31 into Sep 1. + // The phantom annotations sit in the Sep 1 file but carry workday="2025-08-31". + let phantom_start = tz.with_ymd_and_hms(2025, 9, 1, 0, 0, 0).unwrap(); + let phantom_stop = tz.with_ymd_and_hms(2025, 9, 1, 0, 1, 0).unwrap(); + + let stats = compute_monthly_stats_with_workday( + 2025, + 9, + vec![ + (sep1_start, sep1_stop, "2025-09-01"), + (phantom_start, phantom_stop, "2025-08-31"), // must be filtered out + ], + ); + + assert_eq!(stats.total_work_days, 1, "phantom Aug-31 entry must not count as a Sep work day"); + assert_eq!(stats.total_duration_in_minutes, 480, "only real Sep-1 work should be counted"); + } + #[test] fn test_compute_work_stats_multiple_weeks() { let tz = FixedOffset::east_opt(0).unwrap(); From 9c7b52eea8f3bfde5abfe086f15f0afbf043a674 Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 15 Jul 2026 00:01:49 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20rustfmt=20formatting?= =?UTF-8?q?=20in=20work.rs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/gdarquie_work/work.rs | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/plugins/gdarquie_work/work.rs b/src/plugins/gdarquie_work/work.rs index 0cbd893..22d04f5 100644 --- a/src/plugins/gdarquie_work/work.rs +++ b/src/plugins/gdarquie_work/work.rs @@ -372,10 +372,13 @@ mod tests { fn compute_monthly_stats_with_workday( year: i32, month: u32, - pairs: Vec<(chrono::DateTime, chrono::DateTime, &str)>, + pairs: Vec<( + chrono::DateTime, + chrono::DateTime, + &str, + )>, ) -> MonthlyWorkStats { - let target_date = - chrono::NaiveDate::from_ymd_opt(year, month, 1).unwrap(); + let target_date = chrono::NaiveDate::from_ymd_opt(year, month, 1).unwrap(); let mut annotations_hmap: HashMap> = HashMap::new(); for (start_dt, stop_dt, workday) in pairs { @@ -411,10 +414,12 @@ mod tests { year: date.iso_week().year(), week: date.iso_week().week(), }; - let entry = work_stats_by_week.entry(week_id).or_insert(WorkStatsByWeek { - total_duration_in_minutes: 0, - work_stats: vec![], - }); + let entry = work_stats_by_week + .entry(week_id) + .or_insert(WorkStatsByWeek { + total_duration_in_minutes: 0, + work_stats: vec![], + }); entry.total_duration_in_minutes += length; entry.work_stats.push(WorkStats { day: day.clone(), @@ -456,8 +461,14 @@ mod tests { ], ); - assert_eq!(stats.total_work_days, 1, "phantom Aug-31 entry must not count as a Sep work day"); - assert_eq!(stats.total_duration_in_minutes, 480, "only real Sep-1 work should be counted"); + assert_eq!( + stats.total_work_days, 1, + "phantom Aug-31 entry must not count as a Sep work day" + ); + assert_eq!( + stats.total_duration_in_minutes, 480, + "only real Sep-1 work should be counted" + ); } #[test] From 679fd9d8d4900dc902cbceea8946b1323c906ebe Mon Sep 17 00:00:00 2001 From: Amy Date: Wed, 15 Jul 2026 04:11:17 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=8E=A8=20Simplify=20month=20filter=20?= =?UTF-8?q?and=20remove=20over-engineered=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/gdarquie_work/work.rs | 111 +----------------------------- 1 file changed, 2 insertions(+), 109 deletions(-) diff --git a/src/plugins/gdarquie_work/work.rs b/src/plugins/gdarquie_work/work.rs index 22d04f5..70c43fb 100644 --- a/src/plugins/gdarquie_work/work.rs +++ b/src/plugins/gdarquie_work/work.rs @@ -110,11 +110,8 @@ pub fn compute_monthly_work_stats(month: Option<&str>) -> Result = HashMap::new(); @@ -367,110 +364,6 @@ mod tests { assert!(length_in_minutes.contains(&120)); } - // Helper that mimics compute_monthly_work_stats's month-filtering logic. - // Takes a target month (year, month) and a flat list of annotations with explicit workdays. - fn compute_monthly_stats_with_workday( - year: i32, - month: u32, - pairs: Vec<( - chrono::DateTime, - chrono::DateTime, - &str, - )>, - ) -> MonthlyWorkStats { - let target_date = chrono::NaiveDate::from_ymd_opt(year, month, 1).unwrap(); - - let mut annotations_hmap: HashMap> = HashMap::new(); - for (start_dt, stop_dt, workday) in pairs { - let mut a_start = make_annotation(EventName::StartWork, start_dt); - a_start.workday = Some(workday.to_string()); - let mut a_stop = make_annotation(EventName::StopWork, stop_dt); - a_stop.workday = Some(workday.to_string()); - annotations_hmap - .entry(workday.to_string()) - .or_default() - .push(a_start); - annotations_hmap - .entry(workday.to_string()) - .or_default() - .push(a_stop); - } - - // apply the month filter (the fix under test) - annotations_hmap.retain(|workday, _| { - chrono::NaiveDate::parse_from_str(workday, "%Y-%m-%d") - .map(|d| d.year() == target_date.year() && d.month() == target_date.month()) - .unwrap_or(false) - }); - - let mut work_stats_by_week: HashMap = HashMap::new(); - let mut total_duration = 0; - let mut worked_days_set = HashSet::new(); - - for (day, anns) in &annotations_hmap { - let length = compute_work_time_from_annotations(anns); - let date = chrono::NaiveDate::parse_from_str(day, "%Y-%m-%d").unwrap(); - let week_id = WeekId { - year: date.iso_week().year(), - week: date.iso_week().week(), - }; - let entry = work_stats_by_week - .entry(week_id) - .or_insert(WorkStatsByWeek { - total_duration_in_minutes: 0, - work_stats: vec![], - }); - entry.total_duration_in_minutes += length; - entry.work_stats.push(WorkStats { - day: day.clone(), - length_in_minutes: length, - }); - total_duration += length; - worked_days_set.insert(day.clone()); - } - - MonthlyWorkStats { - total_duration_in_minutes: total_duration, - total_work_days: worked_days_set.len() as i32, - work_stats_by_week, - } - } - - /// Regression test: when end_work crosses midnight from the last day of the - /// previous month into the first day of the current month, phantom annotations - /// tagged with the previous month's workday must NOT inflate total_work_days. - #[test] - fn test_work_stats_no_extra_day_when_session_crosses_month_boundary() { - let tz = FixedOffset::east_opt(0).unwrap(); - - // Real session on Sep 1 - let sep1_start = tz.with_ymd_and_hms(2025, 9, 1, 9, 0, 0).unwrap(); - let sep1_stop = tz.with_ymd_and_hms(2025, 9, 1, 17, 0, 0).unwrap(); - - // Phantom session: end_work crossed midnight from Aug 31 into Sep 1. - // The phantom annotations sit in the Sep 1 file but carry workday="2025-08-31". - let phantom_start = tz.with_ymd_and_hms(2025, 9, 1, 0, 0, 0).unwrap(); - let phantom_stop = tz.with_ymd_and_hms(2025, 9, 1, 0, 1, 0).unwrap(); - - let stats = compute_monthly_stats_with_workday( - 2025, - 9, - vec![ - (sep1_start, sep1_stop, "2025-09-01"), - (phantom_start, phantom_stop, "2025-08-31"), // must be filtered out - ], - ); - - assert_eq!( - stats.total_work_days, 1, - "phantom Aug-31 entry must not count as a Sep work day" - ); - assert_eq!( - stats.total_duration_in_minutes, 480, - "only real Sep-1 work should be counted" - ); - } - #[test] fn test_compute_work_stats_multiple_weeks() { let tz = FixedOffset::east_opt(0).unwrap();