kasl-cli 1.0.1

kasl is a comprehensive command-line utility 🛠️ designed to streamline the tracking of work activities 📊, including start times ⏰, pauses ⏸, and task completion
Documentation
#[cfg(test)]
mod tests {
    use chrono::NaiveDate;
    use kasl::db::{pauses::Pauses, workdays::Workdays};
    use serial_test::serial;
    use tempfile::TempDir;
    use test_context::{TestContext, test_context};

    /// Test context for report command tests.
    struct ReportTestContext {
        _temp_dir: TempDir,
    }

    impl TestContext for ReportTestContext {
        /// Sets up a temporary directory for testing database operations.
        fn setup() -> Self {
            let temp_dir = tempfile::tempdir().unwrap();
            // SAFETY: tests touching the env are #[serial] or single-threaded setup
            unsafe {
                std::env::set_var("HOME", temp_dir.path());
            }
            // SAFETY: tests touching the env are #[serial] or single-threaded setup
            unsafe {
                std::env::set_var("LOCALAPPDATA", temp_dir.path());
            }
            ReportTestContext { _temp_dir: temp_dir }
        }
    }

    /// Tests report generation with pauses.
    ///
    /// Simulates a workday with two pauses and verifies that the report is generated correctly.
    #[test_context(ReportTestContext)]
    #[serial]
    #[test]
    fn test_report_with_pauses(_ctx: &mut ReportTestContext) {
        let date = NaiveDate::from_ymd_opt(2025, 6, 24).unwrap();

        // Setup workday
        let mut workdays = Workdays::new().unwrap();
        workdays.insert_start(date).unwrap();

        // Manually update start/end times for deterministic test
        workdays
            .conn
            .execute(
                "UPDATE workdays SET start = '2025-06-24 09:00:00', end = '2025-06-24 17:00:00' WHERE date = ?",
                [&date.to_string()],
            )
            .unwrap();

        // Insert two pauses: 10:00-10:30 and 12:00-13:00.
        let pauses_db = Pauses::new().unwrap();
        let conn = pauses_db.conn.lock();
        conn.execute(
            "INSERT INTO pauses (start, end, duration) VALUES ('2025-06-24 10:00:00', '2025-06-24 10:30:00', ?)",
            [(30 * 60).to_string()],
        )
        .unwrap();
        conn.execute(
            "INSERT INTO pauses (start, end, duration) VALUES ('2025-06-24 12:00:00', '2025-06-24 13:00:00', ?)",
            [(60 * 60).to_string()],
        )
        .unwrap();

        // Drop lock explicitly
        drop(conn);

        let workday = workdays.fetch(date).unwrap().unwrap();
        let pauses_vec = pauses_db.get_daily_pauses(date).unwrap(); // Fetch all pauses
        // tasks variable removed as it's no longer needed with new API

        // Calculate intervals for the test
        let intervals = kasl::libs::report::calculate_work_intervals(&workday, &pauses_vec);
        let output = kasl::libs::report::report_with_intervals(&workday, &intervals);
        assert!(output.is_ok());
    }

    /// Tests report generation without pauses.
    ///
    /// Simulates a workday without pauses and verifies that the report is generated correctly.
    #[test_context(ReportTestContext)]
    #[serial]
    #[test]
    fn test_report_no_pauses(_ctx: &mut ReportTestContext) {
        let date = NaiveDate::from_ymd_opt(2025, 6, 25).unwrap();

        let mut workdays = Workdays::new().unwrap();
        workdays.insert_start(date).unwrap();
        workdays
            .conn
            .execute(
                "UPDATE workdays SET start = '2025-06-25 09:00:00', end = '2025-06-25 17:00:00' WHERE date = ?",
                [&date.to_string()],
            )
            .unwrap();

        let pauses_db = Pauses::new().unwrap();

        let workday = workdays.fetch(date).unwrap().unwrap();
        let pauses_vec = pauses_db.get_daily_pauses(date).unwrap(); // Fetch all pauses
        // tasks variable removed as it's no longer needed with new API

        assert_eq!(pauses_vec.len(), 0);
        // Calculate intervals for the test
        let intervals = kasl::libs::report::calculate_work_intervals(&workday, &pauses_vec);
        let output = kasl::libs::report::report_with_intervals(&workday, &intervals);
        assert!(output.is_ok());
    }

    /// Tests report generation with both breaks and pauses.
    ///
    /// Verifies that manual breaks created with the breaks command are properly
    /// integrated into work interval calculations for reports.
    #[test_context(ReportTestContext)]
    #[serial]
    #[test]
    fn test_report_with_breaks_and_pauses(_ctx: &mut ReportTestContext) {
        let date = NaiveDate::from_ymd_opt(2025, 6, 26).unwrap();

        // Setup workday
        let mut workdays = Workdays::new().unwrap();
        workdays.insert_start(date).unwrap();
        workdays
            .conn
            .execute(
                "UPDATE workdays SET start = '2025-06-26 09:00:00', end = '2025-06-26 17:00:00' WHERE date = ?",
                [&date.to_string()],
            )
            .unwrap();

        // Insert a pause: 10:30-11:00
        let pauses_db = Pauses::new().unwrap();
        let conn = pauses_db.conn.lock();
        conn.execute(
            "INSERT INTO pauses (start, end, duration) VALUES ('2025-06-26 10:30:00', '2025-06-26 11:00:00', ?)",
            [(30 * 60).to_string()],
        )
        .unwrap();
        drop(conn);

        // Insert a manual break: 12:00-13:00 (lunch), now just a protected pause
        pauses_db
            .insert_manual(date.and_hms_opt(12, 0, 0).unwrap(), chrono::Duration::hours(1), true, Some("Lunch break"))
            .unwrap();

        let workday = workdays.fetch(date).unwrap().unwrap();
        let pauses_vec = pauses_db.get_daily_pauses(date).unwrap();

        // Should have both the automatic pause and the manual (protected) pause
        assert_eq!(pauses_vec.len(), 2);

        let intervals = kasl::libs::report::calculate_work_intervals(&workday, &pauses_vec);

        // Should create 3 work intervals:
        // 1. 09:00 - 10:30 (before pause)
        // 2. 11:00 - 12:00 (between pause and break)
        // 3. 13:00 - 17:00 (after break)
        assert_eq!(intervals.len(), 3);

        // Verify the intervals are correctly calculated
        assert_eq!(intervals[0].start, date.and_hms_opt(9, 0, 0).unwrap());
        assert_eq!(intervals[0].end, date.and_hms_opt(10, 30, 0).unwrap());

        assert_eq!(intervals[1].start, date.and_hms_opt(11, 0, 0).unwrap());
        assert_eq!(intervals[1].end, date.and_hms_opt(12, 0, 0).unwrap());

        assert_eq!(intervals[2].start, date.and_hms_opt(13, 0, 0).unwrap());
        assert_eq!(intervals[2].end, date.and_hms_opt(17, 0, 0).unwrap());

        let output = kasl::libs::report::report_with_intervals(&workday, &intervals);
        assert!(output.is_ok());

        // Verify that productivity calculation includes the break
        let (_, productivity) = output.unwrap();
        // The productivity should be calculated considering both the 30-minute pause
        // and the 60-minute break, so total work time should be reduced accordingly
        assert!(productivity > 0.0 && productivity <= 100.0);
    }
}