Skip to main content

meerkat_tools/builtin/
types.rs

1//! Task types for the built-in task management tools
2//!
3//! This module defines the core types for task management including
4//! [`Task`], [`TaskId`], [`TaskStatus`], and [`TaskPriority`].
5
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Task ID - UUID v7 string for unique task identification
11#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct TaskId(pub String);
13
14impl TaskId {
15    /// Create a new TaskId with a generated UUID v7
16    pub fn new() -> Self {
17        Self(meerkat_core::time_compat::new_uuid_v7().to_string())
18    }
19
20    /// Create a TaskId from an existing string
21    pub fn from_string(s: impl Into<String>) -> Self {
22        Self(s.into())
23    }
24}
25
26impl Default for TaskId {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl std::fmt::Display for TaskId {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        write!(f, "{}", self.0)
35    }
36}
37
38impl AsRef<str> for TaskId {
39    fn as_ref(&self) -> &str {
40        &self.0
41    }
42}
43
44/// Status of a task in its lifecycle
45// RMAT-ALLOW(LifecycleSuspicionReport): tool-level task tracking, not a machine boundary
46#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, JsonSchema)]
47#[serde(rename_all = "snake_case")] // for Serialize
48pub enum TaskStatus {
49    /// Task is waiting to be started
50    #[default]
51    Pending,
52    /// Task is actively being worked on
53    InProgress,
54    /// Task has been finished
55    Completed,
56}
57
58impl<'de> Deserialize<'de> for TaskStatus {
59    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
60    where
61        D: serde::Deserializer<'de>,
62    {
63        let raw = String::deserialize(deserializer)?;
64        match raw.as_str() {
65            "pending" => Ok(Self::Pending),
66            "in_progress" => Ok(Self::InProgress),
67            "completed" => Ok(Self::Completed),
68            other => Err(serde::de::Error::custom(format!(
69                "Invalid status: {other}. Must be pending, in_progress, or completed"
70            ))),
71        }
72    }
73}
74
75/// Priority level for a task
76#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, JsonSchema)]
77#[serde(rename_all = "snake_case")] // for Serialize
78pub enum TaskPriority {
79    /// Low priority task
80    Low,
81    /// Medium priority task (default)
82    #[default]
83    Medium,
84    /// High priority task
85    High,
86}
87
88impl<'de> Deserialize<'de> for TaskPriority {
89    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
90    where
91        D: serde::Deserializer<'de>,
92    {
93        let raw = String::deserialize(deserializer)?;
94        match raw.as_str() {
95            "low" => Ok(Self::Low),
96            "medium" => Ok(Self::Medium),
97            "high" => Ok(Self::High),
98            other => Err(serde::de::Error::custom(format!(
99                "Invalid priority: {other}. Must be low, medium, or high"
100            ))),
101        }
102    }
103}
104
105/// A task in the task management system
106#[derive(Clone, Debug, Serialize, Deserialize)]
107pub struct Task {
108    /// Unique identifier for this task
109    pub id: TaskId,
110    /// Short subject/title of the task
111    pub subject: String,
112    /// Detailed description of the task
113    pub description: String,
114    /// Current status of the task
115    pub status: TaskStatus,
116    /// Priority level of the task
117    pub priority: TaskPriority,
118    /// Labels/tags for categorization
119    pub labels: Vec<String>,
120    /// IDs of tasks that this task blocks
121    pub blocks: Vec<TaskId>,
122    /// ISO 8601 timestamp when the task was created
123    pub created_at: String,
124    /// ISO 8601 timestamp when the task was last updated
125    pub updated_at: String,
126    /// Session ID that created this task
127    pub created_by_session: Option<String>,
128    /// Session ID that last updated this task
129    pub updated_by_session: Option<String>,
130    /// Owner/assignee of this task (agent name or user identifier)
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub owner: Option<String>,
133    /// Arbitrary key-value metadata for extensibility
134    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
135    pub metadata: HashMap<String, serde_json::Value>,
136    /// IDs of tasks that block THIS task (inverse of `blocks`)
137    #[serde(default, skip_serializing_if = "Vec::is_empty")]
138    pub blocked_by: Vec<TaskId>,
139}
140
141/// Input for creating a new task
142#[derive(Clone, Debug, Default)]
143pub struct NewTask {
144    /// Short subject/title of the task
145    pub subject: String,
146    /// Detailed description of the task
147    pub description: String,
148    /// Priority level (defaults to Medium if None)
149    pub priority: Option<TaskPriority>,
150    /// Labels/tags for categorization
151    pub labels: Option<Vec<String>>,
152    /// IDs of tasks that this task blocks
153    pub blocks: Option<Vec<TaskId>>,
154    /// Owner/assignee of the task
155    pub owner: Option<String>,
156    /// Arbitrary key-value metadata
157    pub metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
158    /// IDs of tasks that block THIS task
159    pub blocked_by: Option<Vec<TaskId>>,
160}
161
162/// Input for updating an existing task
163#[derive(Clone, Debug, Default)]
164pub struct TaskUpdate {
165    /// New subject/title (if Some)
166    pub subject: Option<String>,
167    /// New description (if Some)
168    pub description: Option<String>,
169    /// New status (if Some)
170    pub status: Option<TaskStatus>,
171    /// New priority (if Some)
172    pub priority: Option<TaskPriority>,
173    /// Replace all labels (if Some)
174    pub labels: Option<Vec<String>>,
175    /// Task IDs to add to blocks
176    pub add_blocks: Option<Vec<TaskId>>,
177    /// Task IDs to remove from blocks
178    pub remove_blocks: Option<Vec<TaskId>>,
179    /// New owner/assignee (if Some)
180    pub owner: Option<String>,
181    /// Metadata key-value pairs to merge (null value = delete key)
182    pub metadata: Option<HashMap<String, serde_json::Value>>,
183    /// Task IDs to add to blocked_by
184    pub add_blocked_by: Option<Vec<TaskId>>,
185    /// Task IDs to remove from blocked_by
186    pub remove_blocked_by: Option<Vec<TaskId>>,
187}
188
189/// Errors that can occur during task operations
190#[derive(Debug, thiserror::Error)]
191pub enum TaskError {
192    /// Task with the given ID was not found
193    #[error("Task not found: {0}")]
194    NotFound(String),
195    /// Error occurred during storage operations
196    #[error("Storage error: {0}")]
197    StorageError(String),
198    /// Invalid task data was provided
199    #[error("Invalid task data: {0}")]
200    InvalidData(String),
201}
202
203/// Metadata about the task store
204#[derive(Clone, Debug, Serialize, Deserialize)]
205pub struct TaskStoreMeta {
206    /// Version of the store format
207    pub version: u32,
208    /// Unique identifier for the project
209    pub project_id: String,
210    /// ISO 8601 timestamp when the store was created
211    pub created_at: String,
212    /// Revision counter for the store (incremented on each write)
213    pub store_rev: u64,
214}
215
216/// Complete serializable state of the task store
217#[derive(Clone, Debug, Serialize, Deserialize)]
218pub struct TaskStoreData {
219    /// Store metadata
220    pub meta: TaskStoreMeta,
221    /// All tasks in the store
222    pub tasks: Vec<Task>,
223}
224
225#[cfg(test)]
226#[allow(clippy::unwrap_used, clippy::expect_used)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn test_task_id_new_generates_uuid() {
232        let id1 = TaskId::new();
233        let id2 = TaskId::new();
234
235        // Should be 36 characters (UUID format)
236        assert_eq!(id1.0.len(), 36);
237        assert_eq!(id2.0.len(), 36);
238
239        // Should be different IDs
240        assert_ne!(id1, id2);
241
242        // Should be valid UUID
243        assert!(uuid::Uuid::parse_str(&id1.0).is_ok());
244    }
245
246    #[test]
247    fn test_task_id_from_string() {
248        let id = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV");
249        assert_eq!(id.0, "01ARZ3NDEKTSV4RRFFQ69G5FAV");
250    }
251
252    #[test]
253    fn test_task_id_display() {
254        let id = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV");
255        assert_eq!(format!("{id}"), "01ARZ3NDEKTSV4RRFFQ69G5FAV");
256    }
257
258    #[test]
259    fn test_task_id_serde() {
260        let id = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV");
261        let json = serde_json::to_string(&id).unwrap();
262        assert_eq!(json, "\"01ARZ3NDEKTSV4RRFFQ69G5FAV\"");
263
264        let parsed: TaskId = serde_json::from_str(&json).unwrap();
265        assert_eq!(parsed, id);
266    }
267
268    #[test]
269    fn test_task_status_serde() {
270        // Test serialization
271        assert_eq!(
272            serde_json::to_string(&TaskStatus::Pending).unwrap(),
273            "\"pending\""
274        );
275        assert_eq!(
276            serde_json::to_string(&TaskStatus::InProgress).unwrap(),
277            "\"in_progress\""
278        );
279        assert_eq!(
280            serde_json::to_string(&TaskStatus::Completed).unwrap(),
281            "\"completed\""
282        );
283
284        // Test deserialization
285        assert_eq!(
286            serde_json::from_str::<TaskStatus>("\"pending\"").unwrap(),
287            TaskStatus::Pending
288        );
289        assert_eq!(
290            serde_json::from_str::<TaskStatus>("\"in_progress\"").unwrap(),
291            TaskStatus::InProgress
292        );
293        assert_eq!(
294            serde_json::from_str::<TaskStatus>("\"completed\"").unwrap(),
295            TaskStatus::Completed
296        );
297    }
298
299    #[test]
300    fn test_task_priority_serde() {
301        // Test serialization
302        assert_eq!(
303            serde_json::to_string(&TaskPriority::Low).unwrap(),
304            "\"low\""
305        );
306        assert_eq!(
307            serde_json::to_string(&TaskPriority::Medium).unwrap(),
308            "\"medium\""
309        );
310        assert_eq!(
311            serde_json::to_string(&TaskPriority::High).unwrap(),
312            "\"high\""
313        );
314
315        // Test deserialization
316        assert_eq!(
317            serde_json::from_str::<TaskPriority>("\"low\"").unwrap(),
318            TaskPriority::Low
319        );
320        assert_eq!(
321            serde_json::from_str::<TaskPriority>("\"medium\"").unwrap(),
322            TaskPriority::Medium
323        );
324        assert_eq!(
325            serde_json::from_str::<TaskPriority>("\"high\"").unwrap(),
326            TaskPriority::High
327        );
328    }
329
330    #[test]
331    fn test_task_status_default() {
332        assert_eq!(TaskStatus::default(), TaskStatus::Pending);
333    }
334
335    #[test]
336    fn test_task_priority_default() {
337        assert_eq!(TaskPriority::default(), TaskPriority::Medium);
338    }
339
340    #[test]
341    fn test_task_serialization_roundtrip() {
342        let task = Task {
343            id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
344            subject: "Implement feature X".to_string(),
345            description: "Add the new feature X to the system".to_string(),
346            status: TaskStatus::InProgress,
347            priority: TaskPriority::High,
348            labels: vec!["feature".to_string(), "urgent".to_string()],
349            blocks: vec![TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAW")],
350            created_at: "2025-01-23T10:00:00Z".to_string(),
351            updated_at: "2025-01-23T11:00:00Z".to_string(),
352            created_by_session: Some("session-123".to_string()),
353            updated_by_session: Some("session-456".to_string()),
354            owner: None,
355            metadata: std::collections::HashMap::new(),
356            blocked_by: vec![],
357        };
358
359        let json = serde_json::to_string_pretty(&task).unwrap();
360        let parsed: Task = serde_json::from_str(&json).unwrap();
361
362        assert_eq!(parsed.id, task.id);
363        assert_eq!(parsed.subject, task.subject);
364        assert_eq!(parsed.description, task.description);
365        assert_eq!(parsed.status, task.status);
366        assert_eq!(parsed.priority, task.priority);
367        assert_eq!(parsed.labels, task.labels);
368        assert_eq!(parsed.blocks, task.blocks);
369        assert_eq!(parsed.created_at, task.created_at);
370        assert_eq!(parsed.updated_at, task.updated_at);
371        assert_eq!(parsed.created_by_session, task.created_by_session);
372        assert_eq!(parsed.updated_by_session, task.updated_by_session);
373    }
374
375    #[test]
376    fn test_task_store_data_serde() {
377        let data = TaskStoreData {
378            meta: TaskStoreMeta {
379                version: 1,
380                project_id: "my-project".to_string(),
381                created_at: "2025-01-23T10:00:00Z".to_string(),
382                store_rev: 42,
383            },
384            tasks: vec![
385                Task {
386                    id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
387                    subject: "Task 1".to_string(),
388                    description: "Description 1".to_string(),
389                    status: TaskStatus::Pending,
390                    priority: TaskPriority::Medium,
391                    labels: vec![],
392                    blocks: vec![],
393                    created_at: "2025-01-23T10:00:00Z".to_string(),
394                    updated_at: "2025-01-23T10:00:00Z".to_string(),
395                    created_by_session: None,
396                    updated_by_session: None,
397                    owner: None,
398                    metadata: std::collections::HashMap::new(),
399                    blocked_by: vec![],
400                },
401                Task {
402                    id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAW"),
403                    subject: "Task 2".to_string(),
404                    description: "Description 2".to_string(),
405                    status: TaskStatus::Completed,
406                    priority: TaskPriority::Low,
407                    labels: vec!["done".to_string()],
408                    blocks: vec![],
409                    created_at: "2025-01-23T11:00:00Z".to_string(),
410                    updated_at: "2025-01-23T12:00:00Z".to_string(),
411                    created_by_session: Some("session-1".to_string()),
412                    updated_by_session: Some("session-2".to_string()),
413                    owner: None,
414                    metadata: std::collections::HashMap::new(),
415                    blocked_by: vec![],
416                },
417            ],
418        };
419
420        let json = serde_json::to_string_pretty(&data).unwrap();
421        let parsed: TaskStoreData = serde_json::from_str(&json).unwrap();
422
423        assert_eq!(parsed.meta.version, data.meta.version);
424        assert_eq!(parsed.meta.project_id, data.meta.project_id);
425        assert_eq!(parsed.meta.created_at, data.meta.created_at);
426        assert_eq!(parsed.meta.store_rev, data.meta.store_rev);
427        assert_eq!(parsed.tasks.len(), 2);
428        assert_eq!(parsed.tasks[0].id, data.tasks[0].id);
429        assert_eq!(parsed.tasks[1].id, data.tasks[1].id);
430    }
431
432    #[test]
433    fn test_task_error_display() {
434        let err = TaskError::NotFound("123".to_string());
435        assert_eq!(err.to_string(), "Task not found: 123");
436
437        let err = TaskError::StorageError("disk full".to_string());
438        assert_eq!(err.to_string(), "Storage error: disk full");
439
440        let err = TaskError::InvalidData("missing subject".to_string());
441        assert_eq!(err.to_string(), "Invalid task data: missing subject");
442    }
443
444    #[test]
445    fn test_task_update_default() {
446        let update = TaskUpdate::default();
447        assert!(update.subject.is_none());
448        assert!(update.description.is_none());
449        assert!(update.status.is_none());
450        assert!(update.priority.is_none());
451        assert!(update.labels.is_none());
452        assert!(update.add_blocks.is_none());
453        assert!(update.remove_blocks.is_none());
454    }
455
456    // ======================================================================
457    // Tests for new TaskUpdate fields: owner, metadata, add_blocked_by, remove_blocked_by
458    // These tests are written TDD-style - they will fail until implementation
459    // ======================================================================
460
461    #[test]
462    fn test_task_update_has_owner_field() {
463        // TaskUpdate should have an optional owner field to set task ownership
464        let update = TaskUpdate {
465            owner: Some("alice".to_string()),
466            ..Default::default()
467        };
468        assert_eq!(update.owner, Some("alice".to_string()));
469
470        // Default should be None
471        let default_update = TaskUpdate::default();
472        assert!(default_update.owner.is_none());
473    }
474
475    #[test]
476    fn test_task_update_has_metadata_field() {
477        // TaskUpdate should have a metadata field for merging key-value pairs
478        // Setting a key to a value adds/updates it
479        // Setting a key to null (Value::Null) removes it
480        let mut metadata = std::collections::HashMap::new();
481        metadata.insert("key1".to_string(), serde_json::json!("value1"));
482        metadata.insert("key2".to_string(), serde_json::json!(42));
483        metadata.insert("key3".to_string(), serde_json::Value::Null); // delete key3
484
485        let update = TaskUpdate {
486            metadata: Some(metadata.clone()),
487            ..Default::default()
488        };
489        assert!(update.metadata.is_some());
490        let meta = update.metadata.unwrap();
491        assert_eq!(meta.get("key1"), Some(&serde_json::json!("value1")));
492        assert_eq!(meta.get("key2"), Some(&serde_json::json!(42)));
493        assert_eq!(meta.get("key3"), Some(&serde_json::Value::Null));
494
495        // Default should be None
496        let default_update = TaskUpdate::default();
497        assert!(default_update.metadata.is_none());
498    }
499
500    #[test]
501    fn test_task_update_has_add_blocked_by_field() {
502        // TaskUpdate should have add_blocked_by to add blocking relationships
503        // blocked_by tracks which tasks block THIS task (inverse of blocks)
504        let update = TaskUpdate {
505            add_blocked_by: Some(vec![
506                TaskId::from_string("blocker-1"),
507                TaskId::from_string("blocker-2"),
508            ]),
509            ..Default::default()
510        };
511        assert!(update.add_blocked_by.is_some());
512        let blocked_by = update.add_blocked_by.unwrap();
513        assert_eq!(blocked_by.len(), 2);
514        assert_eq!(blocked_by[0], TaskId::from_string("blocker-1"));
515        assert_eq!(blocked_by[1], TaskId::from_string("blocker-2"));
516
517        // Default should be None
518        let default_update = TaskUpdate::default();
519        assert!(default_update.add_blocked_by.is_none());
520    }
521
522    #[test]
523    fn test_task_update_has_remove_blocked_by_field() {
524        // TaskUpdate should have remove_blocked_by to remove blocking relationships
525        let update = TaskUpdate {
526            remove_blocked_by: Some(vec![TaskId::from_string("blocker-1")]),
527            ..Default::default()
528        };
529        assert!(update.remove_blocked_by.is_some());
530        let remove_blocked_by = update.remove_blocked_by.unwrap();
531        assert_eq!(remove_blocked_by.len(), 1);
532        assert_eq!(remove_blocked_by[0], TaskId::from_string("blocker-1"));
533
534        // Default should be None
535        let default_update = TaskUpdate::default();
536        assert!(default_update.remove_blocked_by.is_none());
537    }
538
539    #[test]
540    fn test_task_update_all_new_fields_together() {
541        // Test setting all new fields at once
542        let mut metadata = std::collections::HashMap::new();
543        metadata.insert(
544            "priority_reason".to_string(),
545            serde_json::json!("urgent customer request"),
546        );
547
548        let update = TaskUpdate {
549            owner: Some("bob".to_string()),
550            metadata: Some(metadata),
551            add_blocked_by: Some(vec![TaskId::from_string("prerequisite-task")]),
552            remove_blocked_by: Some(vec![TaskId::from_string("old-blocker")]),
553            ..Default::default()
554        };
555
556        assert_eq!(update.owner, Some("bob".to_string()));
557        assert!(update.metadata.is_some());
558        assert!(update.add_blocked_by.is_some());
559        assert!(update.remove_blocked_by.is_some());
560    }
561
562    // ==========================================================================
563    // Tests for new Task struct fields: owner, metadata, blocked_by
564    // Written TDD-style - these will fail until implementation in Task #4
565    // ==========================================================================
566
567    // Task #1: Tests for Task struct with owner field
568    mod task_owner_tests {
569        use super::*;
570
571        #[test]
572        fn test_task_owner_field_exists() {
573            let task = Task {
574                id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
575                subject: "Test task".to_string(),
576                description: "Test description".to_string(),
577                status: TaskStatus::Pending,
578                priority: TaskPriority::Medium,
579                labels: vec![],
580                blocks: vec![],
581                created_at: "2025-01-24T10:00:00Z".to_string(),
582                updated_at: "2025-01-24T10:00:00Z".to_string(),
583                created_by_session: None,
584                updated_by_session: None,
585                owner: None, // New field
586                metadata: std::collections::HashMap::new(),
587                blocked_by: vec![],
588            };
589            assert!(task.owner.is_none());
590        }
591
592        #[test]
593        fn test_task_owner_with_value() {
594            let task = Task {
595                id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
596                subject: "Test task".to_string(),
597                description: "Test description".to_string(),
598                status: TaskStatus::InProgress,
599                priority: TaskPriority::High,
600                labels: vec![],
601                blocks: vec![],
602                created_at: "2025-01-24T10:00:00Z".to_string(),
603                updated_at: "2025-01-24T10:00:00Z".to_string(),
604                created_by_session: None,
605                updated_by_session: None,
606                owner: Some("alice".to_string()),
607                metadata: std::collections::HashMap::new(),
608                blocked_by: vec![],
609            };
610            assert_eq!(task.owner, Some("alice".to_string()));
611        }
612
613        #[test]
614        fn test_task_owner_serialization() {
615            let task = Task {
616                id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
617                subject: "Test task".to_string(),
618                description: "Test".to_string(),
619                status: TaskStatus::Pending,
620                priority: TaskPriority::Medium,
621                labels: vec![],
622                blocks: vec![],
623                created_at: "2025-01-24T10:00:00Z".to_string(),
624                updated_at: "2025-01-24T10:00:00Z".to_string(),
625                created_by_session: None,
626                updated_by_session: None,
627                owner: Some("bob".to_string()),
628                metadata: std::collections::HashMap::new(),
629                blocked_by: vec![],
630            };
631
632            let json = serde_json::to_string(&task).unwrap();
633            assert!(json.contains("\"owner\":\"bob\""));
634
635            let parsed: Task = serde_json::from_str(&json).unwrap();
636            assert_eq!(parsed.owner, Some("bob".to_string()));
637        }
638
639        #[test]
640        fn test_task_owner_none_serialization() {
641            let task = Task {
642                id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
643                subject: "Test task".to_string(),
644                description: "Test".to_string(),
645                status: TaskStatus::Pending,
646                priority: TaskPriority::Medium,
647                labels: vec![],
648                blocks: vec![],
649                created_at: "2025-01-24T10:00:00Z".to_string(),
650                updated_at: "2025-01-24T10:00:00Z".to_string(),
651                created_by_session: None,
652                updated_by_session: None,
653                owner: None,
654                metadata: std::collections::HashMap::new(),
655                blocked_by: vec![],
656            };
657
658            let json = serde_json::to_string(&task).unwrap();
659            let parsed: Task = serde_json::from_str(&json).unwrap();
660            assert_eq!(parsed.owner, None);
661        }
662    }
663
664    // Task #2: Tests for Task struct with metadata field
665    mod task_metadata_tests {
666        use super::*;
667        use std::collections::HashMap;
668
669        #[test]
670        fn test_task_metadata_field_exists() {
671            let task = Task {
672                id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
673                subject: "Test task".to_string(),
674                description: "Test description".to_string(),
675                status: TaskStatus::Pending,
676                priority: TaskPriority::Medium,
677                labels: vec![],
678                blocks: vec![],
679                created_at: "2025-01-24T10:00:00Z".to_string(),
680                updated_at: "2025-01-24T10:00:00Z".to_string(),
681                created_by_session: None,
682                updated_by_session: None,
683                owner: None,
684                metadata: HashMap::new(),
685                blocked_by: vec![],
686            };
687            assert!(task.metadata.is_empty());
688        }
689
690        #[test]
691        fn test_task_metadata_with_values() {
692            let mut metadata = HashMap::new();
693            metadata.insert("priority_score".to_string(), serde_json::json!(42));
694            metadata.insert("estimate_hours".to_string(), serde_json::json!(8.5));
695            metadata.insert(
696                "assignee_email".to_string(),
697                serde_json::json!("alice@example.com"),
698            );
699
700            let task = Task {
701                id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
702                subject: "Test task".to_string(),
703                description: "Test description".to_string(),
704                status: TaskStatus::Pending,
705                priority: TaskPriority::Medium,
706                labels: vec![],
707                blocks: vec![],
708                created_at: "2025-01-24T10:00:00Z".to_string(),
709                updated_at: "2025-01-24T10:00:00Z".to_string(),
710                created_by_session: None,
711                updated_by_session: None,
712                owner: None,
713                metadata,
714                blocked_by: vec![],
715            };
716
717            assert_eq!(task.metadata.len(), 3);
718            assert_eq!(
719                task.metadata.get("priority_score"),
720                Some(&serde_json::json!(42))
721            );
722            assert_eq!(
723                task.metadata.get("estimate_hours"),
724                Some(&serde_json::json!(8.5))
725            );
726        }
727
728        #[test]
729        fn test_task_metadata_with_complex_values() {
730            let mut metadata = HashMap::new();
731            metadata.insert(
732                "tags".to_string(),
733                serde_json::json!(["frontend", "urgent"]),
734            );
735            metadata.insert(
736                "config".to_string(),
737                serde_json::json!({"retries": 3, "timeout": 30}),
738            );
739
740            let task = Task {
741                id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
742                subject: "Test task".to_string(),
743                description: "Test".to_string(),
744                status: TaskStatus::Pending,
745                priority: TaskPriority::Medium,
746                labels: vec![],
747                blocks: vec![],
748                created_at: "2025-01-24T10:00:00Z".to_string(),
749                updated_at: "2025-01-24T10:00:00Z".to_string(),
750                created_by_session: None,
751                updated_by_session: None,
752                owner: None,
753                metadata,
754                blocked_by: vec![],
755            };
756
757            let tags = task.metadata.get("tags").unwrap();
758            assert!(tags.is_array());
759            assert_eq!(tags.as_array().unwrap().len(), 2);
760
761            let config = task.metadata.get("config").unwrap();
762            assert!(config.is_object());
763            assert_eq!(config.get("retries"), Some(&serde_json::json!(3)));
764        }
765
766        #[test]
767        fn test_task_metadata_serialization() {
768            let mut metadata = HashMap::new();
769            metadata.insert("key1".to_string(), serde_json::json!("value1"));
770            metadata.insert("key2".to_string(), serde_json::json!(123));
771
772            let task = Task {
773                id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
774                subject: "Test task".to_string(),
775                description: "Test".to_string(),
776                status: TaskStatus::Pending,
777                priority: TaskPriority::Medium,
778                labels: vec![],
779                blocks: vec![],
780                created_at: "2025-01-24T10:00:00Z".to_string(),
781                updated_at: "2025-01-24T10:00:00Z".to_string(),
782                created_by_session: None,
783                updated_by_session: None,
784                owner: None,
785                metadata,
786                blocked_by: vec![],
787            };
788
789            let json = serde_json::to_string(&task).unwrap();
790            assert!(json.contains("\"metadata\""));
791
792            let parsed: Task = serde_json::from_str(&json).unwrap();
793            assert_eq!(
794                parsed.metadata.get("key1"),
795                Some(&serde_json::json!("value1"))
796            );
797            assert_eq!(parsed.metadata.get("key2"), Some(&serde_json::json!(123)));
798        }
799    }
800
801    // Task #3: Tests for Task struct with blocked_by field
802    mod task_blocked_by_tests {
803        use super::*;
804
805        #[test]
806        fn test_task_blocked_by_field_exists() {
807            let task = Task {
808                id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
809                subject: "Test task".to_string(),
810                description: "Test description".to_string(),
811                status: TaskStatus::Pending,
812                priority: TaskPriority::Medium,
813                labels: vec![],
814                blocks: vec![],
815                created_at: "2025-01-24T10:00:00Z".to_string(),
816                updated_at: "2025-01-24T10:00:00Z".to_string(),
817                created_by_session: None,
818                updated_by_session: None,
819                owner: None,
820                metadata: std::collections::HashMap::new(),
821                blocked_by: vec![],
822            };
823            assert!(task.blocked_by.is_empty());
824        }
825
826        #[test]
827        fn test_task_blocked_by_single_blocker() {
828            let blocker_id = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAW");
829
830            let task = Task {
831                id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
832                subject: "Blocked task".to_string(),
833                description: "This task is blocked by another".to_string(),
834                status: TaskStatus::Pending,
835                priority: TaskPriority::Medium,
836                labels: vec![],
837                blocks: vec![],
838                created_at: "2025-01-24T10:00:00Z".to_string(),
839                updated_at: "2025-01-24T10:00:00Z".to_string(),
840                created_by_session: None,
841                updated_by_session: None,
842                owner: None,
843                metadata: std::collections::HashMap::new(),
844                blocked_by: vec![blocker_id.clone()],
845            };
846
847            assert_eq!(task.blocked_by.len(), 1);
848            assert_eq!(task.blocked_by[0], blocker_id);
849        }
850
851        #[test]
852        fn test_task_blocked_by_multiple_blockers() {
853            let blocker1 = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FA1");
854            let blocker2 = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FA2");
855            let blocker3 = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FA3");
856
857            let task = Task {
858                id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
859                subject: "Multi-blocked task".to_string(),
860                description: "This task is blocked by three others".to_string(),
861                status: TaskStatus::Pending,
862                priority: TaskPriority::High,
863                labels: vec![],
864                blocks: vec![],
865                created_at: "2025-01-24T10:00:00Z".to_string(),
866                updated_at: "2025-01-24T10:00:00Z".to_string(),
867                created_by_session: None,
868                updated_by_session: None,
869                owner: None,
870                metadata: std::collections::HashMap::new(),
871                blocked_by: vec![blocker1.clone(), blocker2.clone(), blocker3.clone()],
872            };
873
874            assert_eq!(task.blocked_by.len(), 3);
875            assert!(task.blocked_by.contains(&blocker1));
876            assert!(task.blocked_by.contains(&blocker2));
877            assert!(task.blocked_by.contains(&blocker3));
878        }
879
880        #[test]
881        fn test_task_blocked_by_serialization() {
882            let blocker_id = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAW");
883
884            let task = Task {
885                id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
886                subject: "Blocked task".to_string(),
887                description: "Test".to_string(),
888                status: TaskStatus::Pending,
889                priority: TaskPriority::Medium,
890                labels: vec![],
891                blocks: vec![],
892                created_at: "2025-01-24T10:00:00Z".to_string(),
893                updated_at: "2025-01-24T10:00:00Z".to_string(),
894                created_by_session: None,
895                updated_by_session: None,
896                owner: None,
897                metadata: std::collections::HashMap::new(),
898                blocked_by: vec![blocker_id.clone()],
899            };
900
901            let json = serde_json::to_string(&task).unwrap();
902            assert!(json.contains("\"blocked_by\""));
903            assert!(json.contains("01ARZ3NDEKTSV4RRFFQ69G5FAW"));
904
905            let parsed: Task = serde_json::from_str(&json).unwrap();
906            assert_eq!(parsed.blocked_by.len(), 1);
907            assert_eq!(parsed.blocked_by[0], blocker_id);
908        }
909
910        #[test]
911        fn test_task_blocks_vs_blocked_by_distinction() {
912            // Task A blocks Task B
913            // From A's perspective: blocks = [B]
914            // From B's perspective: blocked_by = [A]
915
916            let task_a_id = TaskId::from_string("TASK_A_ID_00000000000000");
917            let task_b_id = TaskId::from_string("TASK_B_ID_00000000000000");
918
919            // Task A - the blocker
920            let task_a = Task {
921                id: task_a_id.clone(),
922                subject: "Task A - the blocker".to_string(),
923                description: "This task blocks Task B".to_string(),
924                status: TaskStatus::InProgress,
925                priority: TaskPriority::High,
926                labels: vec![],
927                blocks: vec![task_b_id.clone()], // A blocks B
928                created_at: "2025-01-24T10:00:00Z".to_string(),
929                updated_at: "2025-01-24T10:00:00Z".to_string(),
930                created_by_session: None,
931                updated_by_session: None,
932                owner: None,
933                metadata: std::collections::HashMap::new(),
934                blocked_by: vec![], // A is not blocked by anything
935            };
936
937            // Task B - the blocked task
938            let task_b = Task {
939                id: task_b_id.clone(),
940                subject: "Task B - blocked".to_string(),
941                description: "This task is blocked by Task A".to_string(),
942                status: TaskStatus::Pending,
943                priority: TaskPriority::Medium,
944                labels: vec![],
945                blocks: vec![], // B doesn't block anything
946                created_at: "2025-01-24T10:00:00Z".to_string(),
947                updated_at: "2025-01-24T10:00:00Z".to_string(),
948                created_by_session: None,
949                updated_by_session: None,
950                owner: None,
951                metadata: std::collections::HashMap::new(),
952                blocked_by: vec![task_a_id.clone()], // B is blocked by A
953            };
954
955            // Verify the relationship
956            assert!(task_a.blocks.contains(&task_b_id));
957            assert!(task_a.blocked_by.is_empty());
958
959            assert!(task_b.blocks.is_empty());
960            assert!(task_b.blocked_by.contains(&task_a_id));
961        }
962    }
963}