1use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct TaskId(pub String);
13
14impl TaskId {
15 pub fn new() -> Self {
17 Self(meerkat_core::time_compat::new_uuid_v7().to_string())
18 }
19
20 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#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, JsonSchema)]
47#[serde(rename_all = "snake_case")] pub enum TaskStatus {
49 #[default]
51 Pending,
52 InProgress,
54 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#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, JsonSchema)]
77#[serde(rename_all = "snake_case")] pub enum TaskPriority {
79 Low,
81 #[default]
83 Medium,
84 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#[derive(Clone, Debug, Serialize, Deserialize)]
107pub struct Task {
108 pub id: TaskId,
110 pub subject: String,
112 pub description: String,
114 pub status: TaskStatus,
116 pub priority: TaskPriority,
118 pub labels: Vec<String>,
120 pub blocks: Vec<TaskId>,
122 pub created_at: String,
124 pub updated_at: String,
126 pub created_by_session: Option<String>,
128 pub updated_by_session: Option<String>,
130 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub owner: Option<String>,
133 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
135 pub metadata: HashMap<String, serde_json::Value>,
136 #[serde(default, skip_serializing_if = "Vec::is_empty")]
138 pub blocked_by: Vec<TaskId>,
139}
140
141#[derive(Clone, Debug, Default)]
143pub struct NewTask {
144 pub subject: String,
146 pub description: String,
148 pub priority: Option<TaskPriority>,
150 pub labels: Option<Vec<String>>,
152 pub blocks: Option<Vec<TaskId>>,
154 pub owner: Option<String>,
156 pub metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
158 pub blocked_by: Option<Vec<TaskId>>,
160}
161
162#[derive(Clone, Debug, Default)]
164pub struct TaskUpdate {
165 pub subject: Option<String>,
167 pub description: Option<String>,
169 pub status: Option<TaskStatus>,
171 pub priority: Option<TaskPriority>,
173 pub labels: Option<Vec<String>>,
175 pub add_blocks: Option<Vec<TaskId>>,
177 pub remove_blocks: Option<Vec<TaskId>>,
179 pub owner: Option<String>,
181 pub metadata: Option<HashMap<String, serde_json::Value>>,
183 pub add_blocked_by: Option<Vec<TaskId>>,
185 pub remove_blocked_by: Option<Vec<TaskId>>,
187}
188
189#[derive(Debug, thiserror::Error)]
191pub enum TaskError {
192 #[error("Task not found: {0}")]
194 NotFound(String),
195 #[error("Storage error: {0}")]
197 StorageError(String),
198 #[error("Invalid task data: {0}")]
200 InvalidData(String),
201}
202
203#[derive(Clone, Debug, Serialize, Deserialize)]
205pub struct TaskStoreMeta {
206 pub version: u32,
208 pub project_id: String,
210 pub created_at: String,
212 pub store_rev: u64,
214}
215
216#[derive(Clone, Debug, Serialize, Deserialize)]
218pub struct TaskStoreData {
219 pub meta: TaskStoreMeta,
221 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 assert_eq!(id1.0.len(), 36);
237 assert_eq!(id2.0.len(), 36);
238
239 assert_ne!(id1, id2);
241
242 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 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 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 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 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 #[test]
462 fn test_task_update_has_owner_field() {
463 let update = TaskUpdate {
465 owner: Some("alice".to_string()),
466 ..Default::default()
467 };
468 assert_eq!(update.owner, Some("alice".to_string()));
469
470 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 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); 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 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 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 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 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 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 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 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, 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 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 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 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 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()], 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![], };
936
937 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![], 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()], };
954
955 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}