Skip to main content

meerkat_tools/builtin/
memory_store.rs

1//! In-memory task store for testing
2//!
3//! This module provides [`MemoryTaskStore`], a simple in-memory implementation
4//! of the [`TaskStore`] trait for use in tests.
5
6use super::store::TaskStore;
7use super::types::{NewTask, Task, TaskError, TaskId, TaskStatus, TaskUpdate};
8use async_trait::async_trait;
9
10#[cfg(target_arch = "wasm32")]
11use crate::tokio::sync::RwLock;
12#[cfg(not(target_arch = "wasm32"))]
13use tokio::sync::RwLock;
14
15/// In-memory task store for testing
16///
17/// This store keeps all tasks in memory using a `RwLock<Vec<Task>>`.
18/// It is thread-safe and can be used in async contexts.
19///
20/// # Example
21///
22/// ```text
23/// use meerkat_tools::builtin::{MemoryTaskStore, TaskStore, NewTask};
24///
25/// let store = MemoryTaskStore::new();
26///
27/// let task = store.create(
28///     NewTask {
29///         subject: "Test task".to_string(),
30///         description: "A test task".to_string(),
31///         priority: None,
32///         labels: None,
33///         blocks: None,
34///     },
35///     None
36/// ).await.unwrap();
37///
38/// assert_eq!(store.list().await.unwrap().len(), 1);
39/// ```
40pub struct MemoryTaskStore {
41    tasks: RwLock<Vec<Task>>,
42}
43
44impl MemoryTaskStore {
45    /// Create a new empty memory store
46    pub fn new() -> Self {
47        Self {
48            tasks: RwLock::new(Vec::new()),
49        }
50    }
51
52    /// Create a memory store pre-populated with tasks
53    ///
54    /// This is useful for setting up test fixtures.
55    pub fn with_tasks(tasks: Vec<Task>) -> Self {
56        Self {
57            tasks: RwLock::new(tasks),
58        }
59    }
60
61    /// Get the current number of tasks in the store
62    ///
63    /// This is a convenience method for testing.
64    pub fn len(&self) -> usize {
65        self.tasks.try_read().ok().map_or(0, |t| t.len())
66    }
67
68    /// Check if the store is empty
69    pub fn is_empty(&self) -> bool {
70        self.len() == 0
71    }
72}
73
74impl Default for MemoryTaskStore {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
81#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
82impl TaskStore for MemoryTaskStore {
83    async fn list(&self) -> Result<Vec<Task>, TaskError> {
84        let tasks = self.tasks.read().await;
85        Ok(tasks.clone())
86    }
87
88    async fn get(&self, id: &TaskId) -> Result<Option<Task>, TaskError> {
89        let tasks = self.tasks.read().await;
90        Ok(tasks.iter().find(|t| &t.id == id).cloned())
91    }
92
93    async fn create(&self, new_task: NewTask, session_id: Option<&str>) -> Result<Task, TaskError> {
94        let now = chrono::Utc::now().to_rfc3339();
95        let task = Task {
96            id: TaskId::new(),
97            subject: new_task.subject,
98            description: new_task.description,
99            status: TaskStatus::default(),
100            priority: new_task.priority.unwrap_or_default(),
101            labels: new_task.labels.unwrap_or_default(),
102            blocks: new_task.blocks.unwrap_or_default(),
103            created_at: now.clone(),
104            updated_at: now,
105            created_by_session: session_id.map(String::from),
106            updated_by_session: session_id.map(String::from),
107            owner: new_task.owner,
108            metadata: new_task.metadata.unwrap_or_default(),
109            blocked_by: new_task.blocked_by.unwrap_or_default(),
110        };
111
112        let mut tasks = self.tasks.write().await;
113        tasks.push(task.clone());
114        Ok(task)
115    }
116
117    async fn update(
118        &self,
119        id: &TaskId,
120        update: TaskUpdate,
121        session_id: Option<&str>,
122    ) -> Result<Task, TaskError> {
123        let mut tasks = self.tasks.write().await;
124        let task = tasks
125            .iter_mut()
126            .find(|t| &t.id == id)
127            .ok_or_else(|| TaskError::NotFound(id.0.clone()))?;
128
129        if let Some(subject) = update.subject {
130            task.subject = subject;
131        }
132        if let Some(description) = update.description {
133            task.description = description;
134        }
135        if let Some(status) = update.status {
136            task.status = status;
137        }
138        if let Some(priority) = update.priority {
139            task.priority = priority;
140        }
141        if let Some(labels) = update.labels {
142            task.labels = labels;
143        }
144        if let Some(add_blocks) = update.add_blocks {
145            for block_id in add_blocks {
146                if !task.blocks.contains(&block_id) {
147                    task.blocks.push(block_id);
148                }
149            }
150        }
151        if let Some(remove_blocks) = update.remove_blocks {
152            task.blocks.retain(|b| !remove_blocks.contains(b));
153        }
154
155        // Handle new fields: owner, metadata, add_blocked_by, remove_blocked_by
156        if let Some(owner) = update.owner {
157            task.owner = Some(owner);
158        }
159        if let Some(metadata) = update.metadata {
160            for (key, value) in metadata {
161                if value.is_null() {
162                    // Null value means delete the key
163                    task.metadata.remove(&key);
164                } else {
165                    task.metadata.insert(key, value);
166                }
167            }
168        }
169        if let Some(add_blocked_by) = update.add_blocked_by {
170            for block_id in add_blocked_by {
171                if !task.blocked_by.contains(&block_id) {
172                    task.blocked_by.push(block_id);
173                }
174            }
175        }
176        if let Some(remove_blocked_by) = update.remove_blocked_by {
177            task.blocked_by.retain(|b| !remove_blocked_by.contains(b));
178        }
179
180        task.updated_at = chrono::Utc::now().to_rfc3339();
181        task.updated_by_session = session_id.map(String::from);
182
183        Ok(task.clone())
184    }
185
186    async fn delete(&self, id: &TaskId) -> Result<(), TaskError> {
187        let mut tasks = self.tasks.write().await;
188        let len_before = tasks.len();
189        tasks.retain(|t| &t.id != id);
190        if tasks.len() == len_before {
191            return Err(TaskError::NotFound(id.0.clone()));
192        }
193        Ok(())
194    }
195}
196
197#[cfg(test)]
198#[allow(clippy::unwrap_used, clippy::expect_used)]
199mod tests {
200    use super::*;
201    use crate::builtin::types::{TaskPriority, TaskStatus};
202
203    #[tokio::test]
204    async fn test_memory_store_create_and_get() {
205        let store = MemoryTaskStore::new();
206
207        let new_task = NewTask {
208            subject: "Test task".to_string(),
209            description: "Test description".to_string(),
210            priority: Some(TaskPriority::High),
211            labels: Some(vec!["test".to_string(), "important".to_string()]),
212            blocks: None,
213            ..Default::default()
214        };
215
216        let created = store.create(new_task, Some("session-1")).await.unwrap();
217
218        // Verify created task fields
219        assert_eq!(created.subject, "Test task");
220        assert_eq!(created.description, "Test description");
221        assert_eq!(created.priority, TaskPriority::High);
222        assert_eq!(
223            created.labels,
224            vec!["test".to_string(), "important".to_string()]
225        );
226        assert_eq!(created.status, TaskStatus::Pending);
227        assert_eq!(created.created_by_session, Some("session-1".to_string()));
228        assert_eq!(created.updated_by_session, Some("session-1".to_string()));
229        assert!(!created.created_at.is_empty());
230        assert!(!created.updated_at.is_empty());
231        assert_eq!(created.id.0.len(), 36); // UUID format
232
233        // Verify we can retrieve it
234        let fetched = store.get(&created.id).await.unwrap();
235        assert!(fetched.is_some());
236        let fetched = fetched.unwrap();
237        assert_eq!(fetched.id, created.id);
238        assert_eq!(fetched.subject, created.subject);
239        assert_eq!(fetched.description, created.description);
240    }
241
242    #[tokio::test]
243    async fn test_memory_store_create_with_defaults() {
244        let store = MemoryTaskStore::new();
245
246        let new_task = NewTask {
247            subject: "Simple task".to_string(),
248            description: "No optional fields".to_string(),
249            priority: None,
250            labels: None,
251            blocks: None,
252            ..Default::default()
253        };
254
255        let created = store.create(new_task, None).await.unwrap();
256
257        // Verify defaults are applied
258        assert_eq!(created.priority, TaskPriority::Medium);
259        assert!(created.labels.is_empty());
260        assert!(created.blocks.is_empty());
261        assert!(created.created_by_session.is_none());
262        assert!(created.updated_by_session.is_none());
263    }
264
265    #[tokio::test]
266    async fn test_memory_store_get_nonexistent() {
267        let store = MemoryTaskStore::new();
268
269        let result = store
270            .get(&TaskId::from_string("nonexistent"))
271            .await
272            .unwrap();
273        assert!(result.is_none());
274    }
275
276    #[tokio::test]
277    async fn test_memory_store_list() {
278        let store = MemoryTaskStore::new();
279
280        // Initially empty
281        let tasks = store.list().await.unwrap();
282        assert!(tasks.is_empty());
283
284        // Add some tasks
285        let task1 = NewTask {
286            subject: "Task 1".to_string(),
287            description: "First task".to_string(),
288            priority: Some(TaskPriority::Low),
289            labels: None,
290            blocks: None,
291            ..Default::default()
292        };
293        let task2 = NewTask {
294            subject: "Task 2".to_string(),
295            description: "Second task".to_string(),
296            priority: Some(TaskPriority::High),
297            labels: None,
298            blocks: None,
299            ..Default::default()
300        };
301        let task3 = NewTask {
302            subject: "Task 3".to_string(),
303            description: "Third task".to_string(),
304            priority: None,
305            labels: Some(vec!["urgent".to_string()]),
306            blocks: None,
307            ..Default::default()
308        };
309
310        let created1 = store.create(task1, None).await.unwrap();
311        let created2 = store.create(task2, None).await.unwrap();
312        let created3 = store.create(task3, None).await.unwrap();
313
314        // List should return all tasks
315        let tasks = store.list().await.unwrap();
316        assert_eq!(tasks.len(), 3);
317
318        // Verify all tasks are present
319        let ids: Vec<_> = tasks.iter().map(|t| &t.id).collect();
320        assert!(ids.contains(&&created1.id));
321        assert!(ids.contains(&&created2.id));
322        assert!(ids.contains(&&created3.id));
323    }
324
325    #[tokio::test]
326    async fn test_memory_store_update() {
327        let store = MemoryTaskStore::new();
328
329        let new_task = NewTask {
330            subject: "Original subject".to_string(),
331            description: "Original description".to_string(),
332            priority: Some(TaskPriority::Low),
333            labels: Some(vec!["initial".to_string()]),
334            blocks: None,
335            ..Default::default()
336        };
337
338        let created = store.create(new_task, Some("session-1")).await.unwrap();
339        let original_created_at = created.created_at.clone();
340
341        // Small delay to ensure updated_at is different
342        tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
343
344        // Update the task
345        let update = TaskUpdate {
346            subject: Some("Updated subject".to_string()),
347            description: Some("Updated description".to_string()),
348            status: Some(TaskStatus::InProgress),
349            priority: Some(TaskPriority::High),
350            labels: Some(vec!["updated".to_string(), "reviewed".to_string()]),
351            add_blocks: None,
352            remove_blocks: None,
353            owner: None,
354            metadata: None,
355            add_blocked_by: None,
356            remove_blocked_by: None,
357        };
358
359        let updated = store
360            .update(&created.id, update, Some("session-2"))
361            .await
362            .unwrap();
363
364        // Verify updates
365        assert_eq!(updated.id, created.id);
366        assert_eq!(updated.subject, "Updated subject");
367        assert_eq!(updated.description, "Updated description");
368        assert_eq!(updated.status, TaskStatus::InProgress);
369        assert_eq!(updated.priority, TaskPriority::High);
370        assert_eq!(
371            updated.labels,
372            vec!["updated".to_string(), "reviewed".to_string()]
373        );
374        assert_eq!(updated.created_at, original_created_at); // unchanged
375        assert_eq!(updated.created_by_session, Some("session-1".to_string())); // unchanged
376        assert_eq!(updated.updated_by_session, Some("session-2".to_string()));
377        assert_ne!(updated.updated_at, original_created_at); // changed
378    }
379
380    #[tokio::test]
381    async fn test_memory_store_partial_update() {
382        let store = MemoryTaskStore::new();
383
384        let new_task = NewTask {
385            subject: "Original".to_string(),
386            description: "Original desc".to_string(),
387            priority: Some(TaskPriority::High),
388            labels: Some(vec!["keep".to_string()]),
389            blocks: None,
390            ..Default::default()
391        };
392
393        let created = store.create(new_task, None).await.unwrap();
394
395        // Only update subject
396        let update = TaskUpdate {
397            subject: Some("New subject".to_string()),
398            ..Default::default()
399        };
400
401        let updated = store.update(&created.id, update, None).await.unwrap();
402
403        // Subject changed, everything else unchanged
404        assert_eq!(updated.subject, "New subject");
405        assert_eq!(updated.description, "Original desc");
406        assert_eq!(updated.priority, TaskPriority::High);
407        assert_eq!(updated.labels, vec!["keep".to_string()]);
408        assert_eq!(updated.status, TaskStatus::Pending);
409    }
410
411    #[tokio::test]
412    async fn test_memory_store_update_not_found() {
413        let store = MemoryTaskStore::new();
414
415        let update = TaskUpdate {
416            subject: Some("Updated".to_string()),
417            ..Default::default()
418        };
419
420        let result = store
421            .update(&TaskId::from_string("nonexistent"), update, None)
422            .await;
423
424        assert!(matches!(result, Err(TaskError::NotFound(id)) if id == "nonexistent"));
425    }
426
427    #[tokio::test]
428    async fn test_memory_store_delete() {
429        let store = MemoryTaskStore::new();
430
431        let new_task = NewTask {
432            subject: "To delete".to_string(),
433            description: "Will be deleted".to_string(),
434            priority: None,
435            labels: None,
436            blocks: None,
437            ..Default::default()
438        };
439
440        let created = store.create(new_task, None).await.unwrap();
441
442        // Verify task exists
443        assert!(store.get(&created.id).await.unwrap().is_some());
444        assert_eq!(store.len(), 1);
445
446        // Delete it
447        store.delete(&created.id).await.unwrap();
448
449        // Verify task is gone
450        assert!(store.get(&created.id).await.unwrap().is_none());
451        assert_eq!(store.len(), 0);
452        assert!(store.is_empty());
453    }
454
455    #[tokio::test]
456    async fn test_memory_store_delete_not_found() {
457        let store = MemoryTaskStore::new();
458
459        let result = store.delete(&TaskId::from_string("nonexistent")).await;
460
461        assert!(matches!(result, Err(TaskError::NotFound(id)) if id == "nonexistent"));
462    }
463
464    #[tokio::test]
465    async fn test_memory_store_delete_multiple() {
466        let store = MemoryTaskStore::new();
467
468        // Create multiple tasks
469        for i in 1..=5 {
470            store
471                .create(
472                    NewTask {
473                        subject: format!("Task {i}"),
474                        description: "".to_string(),
475                        priority: None,
476                        labels: None,
477                        blocks: None,
478                        ..Default::default()
479                    },
480                    None,
481                )
482                .await
483                .unwrap();
484        }
485
486        assert_eq!(store.len(), 5);
487
488        let tasks = store.list().await.unwrap();
489
490        // Delete tasks 2 and 4
491        store.delete(&tasks[1].id).await.unwrap();
492        store.delete(&tasks[3].id).await.unwrap();
493
494        assert_eq!(store.len(), 3);
495
496        // Verify correct tasks remain
497        assert!(store.get(&tasks[0].id).await.unwrap().is_some());
498        assert!(store.get(&tasks[1].id).await.unwrap().is_none());
499        assert!(store.get(&tasks[2].id).await.unwrap().is_some());
500        assert!(store.get(&tasks[3].id).await.unwrap().is_none());
501        assert!(store.get(&tasks[4].id).await.unwrap().is_some());
502    }
503
504    #[tokio::test]
505    async fn test_memory_store_add_blocks() {
506        let store = MemoryTaskStore::new();
507
508        // Create two tasks
509        let task1 = store
510            .create(
511                NewTask {
512                    subject: "Task 1".to_string(),
513                    description: "".to_string(),
514                    priority: None,
515                    labels: None,
516                    blocks: None,
517                    ..Default::default()
518                },
519                None,
520            )
521            .await
522            .unwrap();
523
524        let task2 = store
525            .create(
526                NewTask {
527                    subject: "Task 2".to_string(),
528                    description: "".to_string(),
529                    priority: None,
530                    labels: None,
531                    blocks: None,
532                    ..Default::default()
533                },
534                None,
535            )
536            .await
537            .unwrap();
538
539        let task3 = store
540            .create(
541                NewTask {
542                    subject: "Task 3".to_string(),
543                    description: "".to_string(),
544                    priority: None,
545                    labels: None,
546                    blocks: None,
547                    ..Default::default()
548                },
549                None,
550            )
551            .await
552            .unwrap();
553
554        // Add blocks to task1
555        let update = TaskUpdate {
556            add_blocks: Some(vec![task2.id.clone(), task3.id.clone()]),
557            ..Default::default()
558        };
559
560        let updated = store.update(&task1.id, update, None).await.unwrap();
561
562        assert_eq!(updated.blocks.len(), 2);
563        assert!(updated.blocks.contains(&task2.id));
564        assert!(updated.blocks.contains(&task3.id));
565
566        // Adding the same blocks again should not duplicate
567        let update = TaskUpdate {
568            add_blocks: Some(vec![task2.id.clone()]),
569            ..Default::default()
570        };
571
572        let updated = store.update(&task1.id, update, None).await.unwrap();
573        assert_eq!(updated.blocks.len(), 2); // Still 2, not 3
574    }
575
576    #[tokio::test]
577    async fn test_memory_store_remove_blocks() {
578        let store = MemoryTaskStore::new();
579
580        // Create tasks with initial blocks
581        let blocker1 = TaskId::from_string("blocker-1");
582        let blocker2 = TaskId::from_string("blocker-2");
583        let blocker3 = TaskId::from_string("blocker-3");
584
585        let task = store
586            .create(
587                NewTask {
588                    subject: "Main task".to_string(),
589                    description: "".to_string(),
590                    priority: None,
591                    labels: None,
592                    blocks: Some(vec![blocker1.clone(), blocker2.clone(), blocker3.clone()]),
593                    ..Default::default()
594                },
595                None,
596            )
597            .await
598            .unwrap();
599
600        assert_eq!(task.blocks.len(), 3);
601
602        // Remove one block
603        let update = TaskUpdate {
604            remove_blocks: Some(vec![blocker2.clone()]),
605            ..Default::default()
606        };
607
608        let updated = store.update(&task.id, update, None).await.unwrap();
609        assert_eq!(updated.blocks.len(), 2);
610        assert!(updated.blocks.contains(&blocker1));
611        assert!(!updated.blocks.contains(&blocker2));
612        assert!(updated.blocks.contains(&blocker3));
613
614        // Remove multiple blocks
615        let update = TaskUpdate {
616            remove_blocks: Some(vec![blocker1.clone(), blocker3.clone()]),
617            ..Default::default()
618        };
619
620        let updated = store.update(&task.id, update, None).await.unwrap();
621        assert!(updated.blocks.is_empty());
622    }
623
624    #[tokio::test]
625    async fn test_memory_store_add_and_remove_blocks_same_update() {
626        let store = MemoryTaskStore::new();
627
628        let blocker1 = TaskId::from_string("blocker-1");
629        let blocker2 = TaskId::from_string("blocker-2");
630
631        let task = store
632            .create(
633                NewTask {
634                    subject: "Task".to_string(),
635                    description: "".to_string(),
636                    priority: None,
637                    labels: None,
638                    blocks: Some(vec![blocker1.clone()]),
639                    ..Default::default()
640                },
641                None,
642            )
643            .await
644            .unwrap();
645
646        // Add blocker2 and remove blocker1 in the same update
647        let update = TaskUpdate {
648            add_blocks: Some(vec![blocker2.clone()]),
649            remove_blocks: Some(vec![blocker1.clone()]),
650            ..Default::default()
651        };
652
653        let updated = store.update(&task.id, update, None).await.unwrap();
654
655        // add_blocks is processed first, then remove_blocks
656        assert_eq!(updated.blocks.len(), 1);
657        assert!(!updated.blocks.contains(&blocker1));
658        assert!(updated.blocks.contains(&blocker2));
659    }
660
661    #[tokio::test]
662    async fn test_memory_store_with_tasks() {
663        let existing_tasks = vec![
664            Task {
665                id: TaskId::from_string("task-1"),
666                subject: "Existing task 1".to_string(),
667                description: "".to_string(),
668                status: TaskStatus::Completed,
669                priority: TaskPriority::High,
670                labels: vec![],
671                blocks: vec![],
672                owner: None,
673                metadata: std::collections::HashMap::new(),
674                blocked_by: vec![],
675                created_at: "2025-01-01T00:00:00Z".to_string(),
676                updated_at: "2025-01-01T00:00:00Z".to_string(),
677                created_by_session: None,
678                updated_by_session: None,
679            },
680            Task {
681                id: TaskId::from_string("task-2"),
682                subject: "Existing task 2".to_string(),
683                description: "".to_string(),
684                status: TaskStatus::InProgress,
685                priority: TaskPriority::Medium,
686                labels: vec!["work".to_string()],
687                blocks: vec![],
688                owner: None,
689                metadata: std::collections::HashMap::new(),
690                blocked_by: vec![],
691                created_at: "2025-01-02T00:00:00Z".to_string(),
692                updated_at: "2025-01-02T00:00:00Z".to_string(),
693                created_by_session: Some("old-session".to_string()),
694                updated_by_session: Some("old-session".to_string()),
695            },
696        ];
697
698        let store = MemoryTaskStore::with_tasks(existing_tasks);
699
700        assert_eq!(store.len(), 2);
701        assert!(!store.is_empty());
702
703        // Verify we can retrieve pre-populated tasks
704        let task1 = store
705            .get(&TaskId::from_string("task-1"))
706            .await
707            .unwrap()
708            .unwrap();
709        assert_eq!(task1.subject, "Existing task 1");
710        assert_eq!(task1.status, TaskStatus::Completed);
711
712        let task2 = store
713            .get(&TaskId::from_string("task-2"))
714            .await
715            .unwrap()
716            .unwrap();
717        assert_eq!(task2.subject, "Existing task 2");
718        assert_eq!(task2.labels, vec!["work".to_string()]);
719    }
720
721    #[tokio::test]
722    async fn test_memory_store_default() {
723        let store = MemoryTaskStore::default();
724        assert!(store.is_empty());
725        assert_eq!(store.len(), 0);
726    }
727
728    // ======================================================================
729    // Tests for TaskUpdate new fields: owner, metadata, add_blocked_by, remove_blocked_by
730    // These tests verify the store correctly handles the update semantics
731    // ======================================================================
732
733    #[tokio::test]
734    async fn test_memory_store_update_owner() {
735        let store = MemoryTaskStore::new();
736
737        let task = store
738            .create(
739                NewTask {
740                    subject: "Task with no owner".to_string(),
741                    description: "".to_string(),
742                    priority: None,
743                    labels: None,
744                    blocks: None,
745                    ..Default::default()
746                },
747                None,
748            )
749            .await
750            .unwrap();
751
752        // Initially no owner
753        assert!(task.owner.is_none());
754
755        // Set owner
756        let update = TaskUpdate {
757            owner: Some("alice".to_string()),
758            ..Default::default()
759        };
760        let updated = store.update(&task.id, update, None).await.unwrap();
761        assert_eq!(updated.owner, Some("alice".to_string()));
762
763        // Change owner
764        let update = TaskUpdate {
765            owner: Some("bob".to_string()),
766            ..Default::default()
767        };
768        let updated = store.update(&task.id, update, None).await.unwrap();
769        assert_eq!(updated.owner, Some("bob".to_string()));
770
771        // Note: To clear owner, we'd need to support explicit None vs absent
772        // For now, absent owner in update means "don't change"
773    }
774
775    #[tokio::test]
776    async fn test_memory_store_update_metadata_merge() {
777        let store = MemoryTaskStore::new();
778
779        let task = store
780            .create(
781                NewTask {
782                    subject: "Task with metadata".to_string(),
783                    description: "".to_string(),
784                    priority: None,
785                    labels: None,
786                    blocks: None,
787                    ..Default::default()
788                },
789                None,
790            )
791            .await
792            .unwrap();
793
794        // Initially empty metadata
795        assert!(task.metadata.is_empty());
796
797        // Add some metadata
798        let mut metadata = std::collections::HashMap::new();
799        metadata.insert("key1".to_string(), serde_json::json!("value1"));
800        metadata.insert("key2".to_string(), serde_json::json!(100));
801
802        let update = TaskUpdate {
803            metadata: Some(metadata),
804            ..Default::default()
805        };
806        let updated = store.update(&task.id, update, None).await.unwrap();
807
808        assert_eq!(updated.metadata.len(), 2);
809        assert_eq!(
810            updated.metadata.get("key1"),
811            Some(&serde_json::json!("value1"))
812        );
813        assert_eq!(updated.metadata.get("key2"), Some(&serde_json::json!(100)));
814
815        // Merge more metadata - existing keys unchanged, new keys added
816        let mut metadata2 = std::collections::HashMap::new();
817        metadata2.insert("key2".to_string(), serde_json::json!(200)); // update existing
818        metadata2.insert("key3".to_string(), serde_json::json!("new")); // add new
819
820        let update = TaskUpdate {
821            metadata: Some(metadata2),
822            ..Default::default()
823        };
824        let updated = store.update(&task.id, update, None).await.unwrap();
825
826        assert_eq!(updated.metadata.len(), 3);
827        assert_eq!(
828            updated.metadata.get("key1"),
829            Some(&serde_json::json!("value1"))
830        ); // unchanged
831        assert_eq!(updated.metadata.get("key2"), Some(&serde_json::json!(200))); // updated
832        assert_eq!(
833            updated.metadata.get("key3"),
834            Some(&serde_json::json!("new"))
835        ); // added
836    }
837
838    #[tokio::test]
839    async fn test_memory_store_update_metadata_delete_with_null() {
840        let store = MemoryTaskStore::new();
841
842        let task = store
843            .create(
844                NewTask {
845                    subject: "Task with metadata to delete".to_string(),
846                    description: "".to_string(),
847                    priority: None,
848                    labels: None,
849                    blocks: None,
850                    ..Default::default()
851                },
852                None,
853            )
854            .await
855            .unwrap();
856
857        // Add initial metadata
858        let mut metadata = std::collections::HashMap::new();
859        metadata.insert("keep".to_string(), serde_json::json!("keep me"));
860        metadata.insert(
861            "delete_me".to_string(),
862            serde_json::json!("will be deleted"),
863        );
864
865        let update = TaskUpdate {
866            metadata: Some(metadata),
867            ..Default::default()
868        };
869        store.update(&task.id, update, None).await.unwrap();
870
871        // Delete a key by setting it to null
872        let mut metadata_delete = std::collections::HashMap::new();
873        metadata_delete.insert("delete_me".to_string(), serde_json::Value::Null);
874
875        let update = TaskUpdate {
876            metadata: Some(metadata_delete),
877            ..Default::default()
878        };
879        let updated = store.update(&task.id, update, None).await.unwrap();
880
881        // "delete_me" should be removed, "keep" should remain
882        assert_eq!(updated.metadata.len(), 1);
883        assert_eq!(
884            updated.metadata.get("keep"),
885            Some(&serde_json::json!("keep me"))
886        );
887        assert!(!updated.metadata.contains_key("delete_me"));
888    }
889
890    #[tokio::test]
891    async fn test_memory_store_update_add_blocked_by() {
892        let store = MemoryTaskStore::new();
893
894        // Create blocking tasks
895        let blocker1 = store
896            .create(
897                NewTask {
898                    subject: "Blocker 1".to_string(),
899                    description: "".to_string(),
900                    priority: None,
901                    labels: None,
902                    blocks: None,
903                    ..Default::default()
904                },
905                None,
906            )
907            .await
908            .unwrap();
909
910        let blocker2 = store
911            .create(
912                NewTask {
913                    subject: "Blocker 2".to_string(),
914                    description: "".to_string(),
915                    priority: None,
916                    labels: None,
917                    blocks: None,
918                    ..Default::default()
919                },
920                None,
921            )
922            .await
923            .unwrap();
924
925        // Create the blocked task
926        let task = store
927            .create(
928                NewTask {
929                    subject: "Blocked task".to_string(),
930                    description: "".to_string(),
931                    priority: None,
932                    labels: None,
933                    blocks: None,
934                    ..Default::default()
935                },
936                None,
937            )
938            .await
939            .unwrap();
940
941        // Initially no blocked_by
942        assert!(task.blocked_by.is_empty());
943
944        // Add blocker1 to blocked_by
945        let update = TaskUpdate {
946            add_blocked_by: Some(vec![blocker1.id.clone()]),
947            ..Default::default()
948        };
949        let updated = store.update(&task.id, update, None).await.unwrap();
950
951        assert_eq!(updated.blocked_by.len(), 1);
952        assert!(updated.blocked_by.contains(&blocker1.id));
953
954        // Add blocker2 (blocker1 should still be there)
955        let update = TaskUpdate {
956            add_blocked_by: Some(vec![blocker2.id.clone()]),
957            ..Default::default()
958        };
959        let updated = store.update(&task.id, update, None).await.unwrap();
960
961        assert_eq!(updated.blocked_by.len(), 2);
962        assert!(updated.blocked_by.contains(&blocker1.id));
963        assert!(updated.blocked_by.contains(&blocker2.id));
964
965        // Adding same blocker again should not duplicate
966        let update = TaskUpdate {
967            add_blocked_by: Some(vec![blocker1.id.clone()]),
968            ..Default::default()
969        };
970        let updated = store.update(&task.id, update, None).await.unwrap();
971
972        assert_eq!(updated.blocked_by.len(), 2); // Still 2, no duplicate
973    }
974
975    #[tokio::test]
976    async fn test_memory_store_update_remove_blocked_by() {
977        let store = MemoryTaskStore::new();
978
979        let blocker1 = TaskId::from_string("blocker-1");
980        let blocker2 = TaskId::from_string("blocker-2");
981        let blocker3 = TaskId::from_string("blocker-3");
982
983        // Create task with initial blocked_by
984        // Note: This requires Task to have blocked_by field - will fail until implemented
985        let task = store
986            .create(
987                NewTask {
988                    subject: "Task blocked by multiple".to_string(),
989                    description: "".to_string(),
990                    priority: None,
991                    labels: None,
992                    blocks: None,
993                    ..Default::default()
994                },
995                None,
996            )
997            .await
998            .unwrap();
999
1000        // First add some blocked_by entries
1001        let update = TaskUpdate {
1002            add_blocked_by: Some(vec![blocker1.clone(), blocker2.clone(), blocker3.clone()]),
1003            ..Default::default()
1004        };
1005        let task = store.update(&task.id, update, None).await.unwrap();
1006        assert_eq!(task.blocked_by.len(), 3);
1007
1008        // Remove one
1009        let update = TaskUpdate {
1010            remove_blocked_by: Some(vec![blocker2.clone()]),
1011            ..Default::default()
1012        };
1013        let updated = store.update(&task.id, update, None).await.unwrap();
1014
1015        assert_eq!(updated.blocked_by.len(), 2);
1016        assert!(updated.blocked_by.contains(&blocker1));
1017        assert!(!updated.blocked_by.contains(&blocker2));
1018        assert!(updated.blocked_by.contains(&blocker3));
1019
1020        // Remove multiple
1021        let update = TaskUpdate {
1022            remove_blocked_by: Some(vec![blocker1.clone(), blocker3.clone()]),
1023            ..Default::default()
1024        };
1025        let updated = store.update(&task.id, update, None).await.unwrap();
1026
1027        assert!(updated.blocked_by.is_empty());
1028    }
1029
1030    #[tokio::test]
1031    async fn test_memory_store_update_add_and_remove_blocked_by_same_update() {
1032        let store = MemoryTaskStore::new();
1033
1034        let blocker1 = TaskId::from_string("blocker-1");
1035        let blocker2 = TaskId::from_string("blocker-2");
1036
1037        let task = store
1038            .create(
1039                NewTask {
1040                    subject: "Task".to_string(),
1041                    description: "".to_string(),
1042                    priority: None,
1043                    labels: None,
1044                    blocks: None,
1045                    ..Default::default()
1046                },
1047                None,
1048            )
1049            .await
1050            .unwrap();
1051
1052        // Add blocker1 first
1053        let update = TaskUpdate {
1054            add_blocked_by: Some(vec![blocker1.clone()]),
1055            ..Default::default()
1056        };
1057        store.update(&task.id, update, None).await.unwrap();
1058
1059        // Add blocker2 and remove blocker1 in the same update
1060        let update = TaskUpdate {
1061            add_blocked_by: Some(vec![blocker2.clone()]),
1062            remove_blocked_by: Some(vec![blocker1.clone()]),
1063            ..Default::default()
1064        };
1065        let updated = store.update(&task.id, update, None).await.unwrap();
1066
1067        // add_blocked_by is processed first, then remove_blocked_by
1068        assert_eq!(updated.blocked_by.len(), 1);
1069        assert!(!updated.blocked_by.contains(&blocker1));
1070        assert!(updated.blocked_by.contains(&blocker2));
1071    }
1072}