kasl-cli 1.10.0

Work activity tracker CLI: automatic workday and break detection, task management with Jira/GitLab integration, productivity reports and exports
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
//! End-to-end scenarios across the storage layers.
//!
//! A day is not one table: starting it writes a workday, the tasks recorded
//! against it live elsewhere, and the config that drives both is a third
//! store. These tests drive whole sequences - open a day, record tasks,
//! close it, reopen the database - to catch the seams between them.

#[cfg(test)]
mod tests {
    use chrono::{Duration, Utc};
    use kasl::db::db::Db;
    use kasl::db::tasks::Tasks;
    use kasl::db::workdays::Workdays;
    use kasl::libs::config::Config;
    use kasl::libs::task::{Task, TaskFilter};
    use serial_test::serial;
    use tempfile::TempDir;
    use test_context::{TestContext, test_context};

    struct WorkflowTestContext {
        _temp_dir: TempDir,
    }

    impl TestContext for WorkflowTestContext {
        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());
            }
            WorkflowTestContext { _temp_dir: temp_dir }
        }
    }

    #[test_context(WorkflowTestContext)]
    #[serial]
    #[test]
    fn test_complete_work_session_workflow(_ctx: &mut WorkflowTestContext) {
        // 1. Initialize database
        let _db = Db::new().unwrap();

        // 2. Create workdays and tasks components
        let mut workdays = Workdays::new().unwrap();
        let mut tasks = Tasks::new().unwrap();

        // 3. Create a workday
        let today = Utc::now().date_naive();
        workdays.insert_start(today).unwrap();

        // 4. Create some tasks for the day
        let task1 = Task::new("Morning standup", "Daily team meeting", Some(25));
        let task2 = Task::new("Code review", "Review PR #123", Some(50));
        let task3 = Task::new("Bug fix", "Fix login issue", Some(75));

        tasks.insert(&task1).unwrap();
        tasks.insert(&task2).unwrap();
        tasks.insert(&task3).unwrap();

        // 5. Complete some tasks during the day
        let task_list = tasks.fetch(kasl::libs::task::TaskFilter::All).unwrap();
        assert_eq!(task_list.len(), 3);

        // Mark first two tasks as complete
        let mut completed_task1 = task_list[0].clone();
        let mut completed_task2 = task_list[1].clone();

        completed_task1.completeness = Some(100);
        completed_task2.completeness = Some(100);

        tasks.update(&completed_task1).unwrap();
        tasks.update(&completed_task2).unwrap();

        // 6. End the workday
        workdays.insert_end(today).unwrap();

        // 7. Verify the workflow completed successfully
        let final_workday = workdays.fetch(today).unwrap();
        assert!(final_workday.is_some());
        assert!(final_workday.unwrap().end.is_some());

        let final_tasks = tasks.fetch(kasl::libs::task::TaskFilter::All).unwrap();
        let completed_tasks: Vec<_> = final_tasks.iter().filter(|t| t.completeness == Some(100)).collect();
        assert_eq!(completed_tasks.len(), 2); // task1 and task2 completed
    }

    #[test_context(WorkflowTestContext)]
    #[serial]
    #[test]
    fn test_multi_day_workflow(_ctx: &mut WorkflowTestContext) {
        let _db = Db::new().unwrap();
        let mut workdays = Workdays::new().unwrap();
        let mut tasks = Tasks::new().unwrap();

        let base_date = Utc::now() - Duration::days(3);

        // Create workdays for 3 consecutive days
        for day in 0..3 {
            let day_date = (base_date + Duration::days(day)).date_naive();
            workdays.insert_start(day_date).unwrap();

            // Create task for each day
            let task_name = format!("Day {} Task", day + 1);
            let task = Task::new(&task_name, "Daily task", Some(50));
            tasks.insert(&task).unwrap();

            // Complete the task
            let task_list = tasks.fetch(kasl::libs::task::TaskFilter::All).unwrap();
            let mut last_task = task_list.last().unwrap().clone();
            last_task.completeness = Some(100);
            tasks.update(&last_task).unwrap();

            // End the workday
            workdays.insert_end(day_date).unwrap();
        }

        // Verify all tasks were created
        let all_tasks = tasks.fetch(kasl::libs::task::TaskFilter::All).unwrap();
        assert!(all_tasks.len() >= 3);

        let completed_tasks: Vec<_> = all_tasks.iter().filter(|t| t.completeness == Some(100)).collect();
        assert!(completed_tasks.len() >= 3);
    }

    #[test_context(WorkflowTestContext)]
    #[serial]
    #[test]
    fn test_configuration_integration(_ctx: &mut WorkflowTestContext) {
        // 1. Create and save a configuration
        let config = Config {
            monitor: Some(kasl::libs::config::MonitorConfig {
                min_pause_duration: 30,
                pause_threshold: 60,
                poll_interval: 1000,
                activity_threshold: 30,
                min_work_interval: 10,
                ..Default::default()
            }),
            server: Some(kasl::libs::config::ServerConfig {
                api_url: "https://test.example.com".to_string(),
                auth_token: "test_token_123".to_string(),
            }),
            kasl_server: None,
            si: None,
            gitlab: None,
            jira: None,
            jira_inbox: None,
            productivity: None,
            report: None,
            task_discovery: None,
        };

        let save_result = config.save();
        assert!(save_result.is_ok());

        // 2. Read configuration back
        let loaded_config = Config::read().unwrap();

        assert!(loaded_config.monitor.is_some());
        assert!(loaded_config.server.is_some());

        let monitor_config = loaded_config.monitor.unwrap();
        assert_eq!(monitor_config.min_pause_duration, 30);
        assert_eq!(monitor_config.pause_threshold, 60);

        let server_config = loaded_config.server.unwrap();
        assert_eq!(server_config.api_url, "https://test.example.com");
        assert_eq!(server_config.auth_token, "test_token_123");

        // 3. Verify configuration integrates with database operations
        let _db = Db::new().unwrap();
        let mut tasks = Tasks::new().unwrap();

        // Tasks should work with configuration loaded
        let task = Task::new("Config Test Task", "Test with config loaded", Some(60));
        let result = tasks.insert(&task);
        assert!(result.is_ok());
    }

    #[test_context(WorkflowTestContext)]
    #[serial]
    #[test]
    fn test_database_consistency(_ctx: &mut WorkflowTestContext) {
        // Test that database operations maintain consistency
        let _db = Db::new().unwrap();
        let mut workdays = Workdays::new().unwrap();
        let mut tasks = Tasks::new().unwrap();

        // Create workday for today
        let today = Utc::now().date_naive();
        workdays.insert_start(today).unwrap();

        // Create tasks
        for i in 1..=5 {
            let task = Task::new(&format!("Task {}", i), &format!("Description {}", i), Some(i * 20));
            tasks.insert(&task).unwrap();
        }

        // Complete every other task
        let task_list = tasks.fetch(kasl::libs::task::TaskFilter::All).unwrap();
        for (index, task) in task_list.iter().enumerate() {
            if index % 2 == 0 {
                let mut completed_task = task.clone();
                completed_task.completeness = Some(100);
                tasks.update(&completed_task).unwrap();
            }
        }

        // End workday
        workdays.insert_end(today).unwrap();

        // Verify consistency
        let final_workday = workdays.fetch(today).unwrap();
        let final_tasks = tasks.fetch(kasl::libs::task::TaskFilter::All).unwrap();

        assert!(final_workday.is_some());
        assert_eq!(final_tasks.len(), 5);

        let completed_count = final_tasks.iter().filter(|t| t.completeness == Some(100)).count();
        assert_eq!(completed_count, 3); // Tasks 1, 3, and 5 (indices 0, 2, 4)
    }

    #[test_context(WorkflowTestContext)]
    #[serial]
    #[test]
    fn test_error_recovery_workflow(_ctx: &mut WorkflowTestContext) {
        let _db = Db::new().unwrap();
        let mut tasks = Tasks::new().unwrap();

        // 1. Create valid data first
        let valid_task = Task::new("Valid task", "This should work", Some(50));
        let valid_result = tasks.insert(&valid_task);
        assert!(valid_result.is_ok());

        // 2. Test operations with edge cases
        let edge_cases = vec![
            Task::new("", "", Some(0)), // Empty strings
            Task::new(
                "Very long task name that might exceed some theoretical limit but should still work fine in most database systems",
                "Also a very long description",
                Some(100),
            ), // Long strings
            Task::new("Special chars: àáâãäå ñü", "Unicode test: 你好世界", Some(25)), // Unicode
        ];

        for edge_task in edge_cases {
            let result = tasks.insert(&edge_task);
            // Results may vary, but operations should not crash
            match result {
                Ok(_) => {
                    // If successful, continue
                }
                Err(_) => {
                    // If error, that's also acceptable for edge cases
                }
            }
        }

        // 3. Verify that valid operations still work after edge cases
        let another_valid_task = Task::new("Another valid task", "This should also work", Some(75));
        let another_valid_result = tasks.insert(&another_valid_task);
        assert!(another_valid_result.is_ok());

        let all_tasks = tasks.fetch(kasl::libs::task::TaskFilter::All).unwrap();
        assert!(all_tasks.len() >= 2); // At least the two valid tasks
    }

    #[test_context(WorkflowTestContext)]
    #[serial]
    #[test]
    fn test_concurrent_access_simulation(_ctx: &mut WorkflowTestContext) {
        // Simulate concurrent access by creating multiple database handles
        let _db1 = Db::new().unwrap();
        let _db2 = Db::new().unwrap();

        let mut tasks1 = Tasks::new().unwrap();
        let mut tasks2 = Tasks::new().unwrap();
        let mut workdays1 = Workdays::new().unwrap();
        let mut workdays2 = Workdays::new().unwrap();

        // Create data through first handle
        let task1 = Task::new("From handle 1", "Task via first database handle", Some(30));
        let yesterday = (Utc::now() - Duration::days(1)).date_naive();
        workdays1.insert_start(yesterday).unwrap();
        tasks1.insert(&task1).unwrap();

        // Create data through second handle
        let task2 = Task::new("From handle 2", "Task via second database handle", Some(70));
        let today = Utc::now().date_naive();
        workdays2.insert_start(today).unwrap();
        tasks2.insert(&task2).unwrap();

        // Both handles should see all data
        let tasks_via_handle1 = tasks1.fetch(kasl::libs::task::TaskFilter::All).unwrap();
        let tasks_via_handle2 = tasks2.fetch(kasl::libs::task::TaskFilter::All).unwrap();

        assert_eq!(tasks_via_handle1.len(), tasks_via_handle2.len());
        assert!(tasks_via_handle1.len() >= 2);

        // Clean up workdays
        workdays1.insert_end(yesterday).unwrap();
        workdays2.insert_end(today).unwrap();
    }

    #[test_context(WorkflowTestContext)]
    #[serial]
    #[test]
    fn test_data_persistence_across_sessions(_ctx: &mut WorkflowTestContext) {
        // First session: create data
        {
            let _db = Db::new().unwrap();
            let mut tasks = Tasks::new().unwrap();
            let mut workdays = Workdays::new().unwrap();

            let task = Task::new("Persistent Task", "Should survive across sessions", Some(40));
            let today = Utc::now().date_naive();
            workdays.insert_start(today).unwrap();
            tasks.insert(&task).unwrap();
            workdays.insert_end(today).unwrap();
        } // Database connection closes here

        // Second session: verify data persists
        {
            let _db = Db::new().unwrap();
            let mut tasks = Tasks::new().unwrap();
            let mut workdays = Workdays::new().unwrap();

            let persisted_tasks = tasks.fetch(kasl::libs::task::TaskFilter::All).unwrap();
            let today = Utc::now().date_naive();
            let persisted_workday = workdays.fetch(today).unwrap();

            assert!(!persisted_tasks.is_empty());
            assert!(persisted_workday.is_some());

            let persistent_task = persisted_tasks.iter().find(|t| t.name == "Persistent Task");
            assert!(persistent_task.is_some());
        }
    }

    #[test_context(WorkflowTestContext)]
    #[serial]
    #[test]
    fn test_task_lifecycle_management(_ctx: &mut WorkflowTestContext) {
        let _db = Db::new().unwrap();
        let mut tasks = Tasks::new().unwrap();

        // Create tasks with different completion levels
        let task1 = Task::new("New Task", "Just created", Some(0));
        let task2 = Task::new("In Progress Task", "Half done", Some(50));
        let task3 = Task::new("Almost Done Task", "Nearly finished", Some(90));

        tasks.insert(&task1).unwrap();
        tasks.insert(&task2).unwrap();
        tasks.insert(&task3).unwrap();

        // Get all tasks
        let all_tasks = tasks.fetch(kasl::libs::task::TaskFilter::All).unwrap();
        assert_eq!(all_tasks.len(), 3);

        // Complete the first task
        let mut task_to_complete = all_tasks[0].clone();
        task_to_complete.completeness = Some(100);
        let update_result = tasks.update(&task_to_complete);
        assert!(update_result.is_ok());

        // Verify completion
        let updated_tasks = tasks.fetch(kasl::libs::task::TaskFilter::All).unwrap();
        let completed_count = updated_tasks.iter().filter(|t| t.completeness == Some(100)).count();
        assert_eq!(completed_count, 1);
    }

    #[test_context(WorkflowTestContext)]
    #[serial]
    #[test]
    fn test_workday_time_management(_ctx: &mut WorkflowTestContext) {
        let _db = Db::new().unwrap();
        let mut workdays = Workdays::new().unwrap();

        // Test creating workdays for different dates
        let today = Utc::now().date_naive();
        let yesterday = (Utc::now() - Duration::days(1)).date_naive();

        // Create workdays
        workdays.insert_start(yesterday).unwrap();
        workdays.insert_start(today).unwrap();

        // End workdays
        workdays.insert_end(yesterday).unwrap();
        workdays.insert_end(today).unwrap();

        // Verify both workdays exist and are completed
        let yesterday_workday = workdays.fetch(yesterday).unwrap();
        let today_workday = workdays.fetch(today).unwrap();

        assert!(yesterday_workday.is_some());
        assert!(today_workday.is_some());
        assert!(yesterday_workday.unwrap().end.is_some());
        assert!(today_workday.unwrap().end.is_some());
    }

    #[test_context(WorkflowTestContext)]
    #[serial]
    #[test]
    fn test_error_handling_and_recovery(_ctx: &mut WorkflowTestContext) {
        let _db = Db::new().unwrap();
        let mut tasks = Tasks::new().unwrap();

        // Test creating a valid task
        let task = Task::new("Valid task", "This should work", Some(50));
        let result = tasks.insert(&task);
        assert!(result.is_ok());

        // Test operations on non-existent IDs
        let non_existent_task = tasks.get_by_id(99999).unwrap();
        assert!(non_existent_task.is_none());

        // Test that valid operations still work after error cases
        let another_task = Task::new("Another task", "Should still work", Some(75));
        let result2 = tasks.insert(&another_task);
        assert!(result2.is_ok());

        let all_tasks = tasks.fetch(kasl::libs::task::TaskFilter::All).unwrap();
        assert_eq!(all_tasks.len(), 2);
    }

    #[test_context(WorkflowTestContext)]
    #[serial]
    #[test]
    fn the_link_between_an_issue_and_its_task_survives_a_rename(_ctx: &mut WorkflowTestContext) {
        // The key used to live only inside the task's name, so renaming the
        // task lost which issue it came from. `jira_key` is what the lookup
        // goes through now, and the UPDATE statement does not list the column
        // at all - an edit cannot reach it, whatever the caller passes.
        let mut tasks = Tasks::new().unwrap();
        let task = Task::new("PROJ-412 Fix session timeout", "", Some(0)).from_jira("PROJ-412");
        tasks.insert(&task).unwrap();

        let found = tasks.fetch(TaskFilter::ByJiraKey("PROJ-412".to_string())).unwrap();
        assert_eq!(found.len(), 1, "the task must be findable by its issue key");
        let id = found[0].id.unwrap();

        // Rename it to something that no longer mentions the key at all, and
        // pass a task whose `jira_key` has been cleared - as `task edit` would
        // if it ever stopped copying the field forward.
        let mut renamed = found[0].clone();
        renamed.update_from(&Task::new("Session timeout on settings", "stale cookie jar", Some(60)));
        renamed.jira_key = None;
        tasks.update(&renamed).unwrap();

        let after = tasks.fetch(TaskFilter::ByJiraKey("PROJ-412".to_string())).unwrap();
        assert_eq!(after.len(), 1, "renaming must not detach the task from its issue");
        assert_eq!(after[0].id.unwrap(), id);
        assert_eq!(after[0].name, "Session timeout on settings");
        assert_eq!(after[0].jira_key.as_deref(), Some("PROJ-412"));
    }

    #[test_context(WorkflowTestContext)]
    #[serial]
    #[test]
    fn a_task_not_taken_from_an_issue_carries_no_key(_ctx: &mut WorkflowTestContext) {
        let mut tasks = Tasks::new().unwrap();
        tasks.insert(&Task::new("Something I thought of myself", "", Some(0))).unwrap();

        let all = tasks.fetch(TaskFilter::All).unwrap();
        assert_eq!(all.len(), 1);
        assert!(all[0].jira_key.is_none(), "only `inbox take` sets a key");
        assert!(tasks.fetch(TaskFilter::ByJiraKey("PROJ-1".to_string())).unwrap().is_empty());
    }
}