use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TaskId(pub String);
impl TaskId {
pub fn new() -> Self {
Self(uuid::Uuid::now_v7().to_string())
}
pub fn from_string(s: impl Into<String>) -> Self {
Self(s.into())
}
}
impl Default for TaskId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for TaskId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for TaskId {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")] pub enum TaskStatus {
#[default]
Pending,
InProgress,
Completed,
}
impl<'de> Deserialize<'de> for TaskStatus {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
match raw.as_str() {
"pending" => Ok(Self::Pending),
"in_progress" => Ok(Self::InProgress),
"completed" => Ok(Self::Completed),
other => Err(serde::de::Error::custom(format!(
"Invalid status: {}. Must be pending, in_progress, or completed",
other
))),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")] pub enum TaskPriority {
Low,
#[default]
Medium,
High,
}
impl<'de> Deserialize<'de> for TaskPriority {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
match raw.as_str() {
"low" => Ok(Self::Low),
"medium" => Ok(Self::Medium),
"high" => Ok(Self::High),
other => Err(serde::de::Error::custom(format!(
"Invalid priority: {}. Must be low, medium, or high",
other
))),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Task {
pub id: TaskId,
pub subject: String,
pub description: String,
pub status: TaskStatus,
pub priority: TaskPriority,
pub labels: Vec<String>,
pub blocks: Vec<TaskId>,
pub created_at: String,
pub updated_at: String,
pub created_by_session: Option<String>,
pub updated_by_session: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub blocked_by: Vec<TaskId>,
}
#[derive(Clone, Debug, Default)]
pub struct NewTask {
pub subject: String,
pub description: String,
pub priority: Option<TaskPriority>,
pub labels: Option<Vec<String>>,
pub blocks: Option<Vec<TaskId>>,
pub owner: Option<String>,
pub metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
pub blocked_by: Option<Vec<TaskId>>,
}
#[derive(Clone, Debug, Default)]
pub struct TaskUpdate {
pub subject: Option<String>,
pub description: Option<String>,
pub status: Option<TaskStatus>,
pub priority: Option<TaskPriority>,
pub labels: Option<Vec<String>>,
pub add_blocks: Option<Vec<TaskId>>,
pub remove_blocks: Option<Vec<TaskId>>,
pub owner: Option<String>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
pub add_blocked_by: Option<Vec<TaskId>>,
pub remove_blocked_by: Option<Vec<TaskId>>,
}
#[derive(Debug, thiserror::Error)]
pub enum TaskError {
#[error("Task not found: {0}")]
NotFound(String),
#[error("Storage error: {0}")]
StorageError(String),
#[error("Invalid task data: {0}")]
InvalidData(String),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TaskStoreMeta {
pub version: u32,
pub project_id: String,
pub created_at: String,
pub store_rev: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TaskStoreData {
pub meta: TaskStoreMeta,
pub tasks: Vec<Task>,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_task_id_new_generates_uuid() {
let id1 = TaskId::new();
let id2 = TaskId::new();
assert_eq!(id1.0.len(), 36);
assert_eq!(id2.0.len(), 36);
assert_ne!(id1, id2);
assert!(uuid::Uuid::parse_str(&id1.0).is_ok());
}
#[test]
fn test_task_id_from_string() {
let id = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV");
assert_eq!(id.0, "01ARZ3NDEKTSV4RRFFQ69G5FAV");
}
#[test]
fn test_task_id_display() {
let id = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV");
assert_eq!(format!("{}", id), "01ARZ3NDEKTSV4RRFFQ69G5FAV");
}
#[test]
fn test_task_id_serde() {
let id = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV");
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, "\"01ARZ3NDEKTSV4RRFFQ69G5FAV\"");
let parsed: TaskId = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, id);
}
#[test]
fn test_task_status_serde() {
assert_eq!(
serde_json::to_string(&TaskStatus::Pending).unwrap(),
"\"pending\""
);
assert_eq!(
serde_json::to_string(&TaskStatus::InProgress).unwrap(),
"\"in_progress\""
);
assert_eq!(
serde_json::to_string(&TaskStatus::Completed).unwrap(),
"\"completed\""
);
assert_eq!(
serde_json::from_str::<TaskStatus>("\"pending\"").unwrap(),
TaskStatus::Pending
);
assert_eq!(
serde_json::from_str::<TaskStatus>("\"in_progress\"").unwrap(),
TaskStatus::InProgress
);
assert_eq!(
serde_json::from_str::<TaskStatus>("\"completed\"").unwrap(),
TaskStatus::Completed
);
}
#[test]
fn test_task_priority_serde() {
assert_eq!(
serde_json::to_string(&TaskPriority::Low).unwrap(),
"\"low\""
);
assert_eq!(
serde_json::to_string(&TaskPriority::Medium).unwrap(),
"\"medium\""
);
assert_eq!(
serde_json::to_string(&TaskPriority::High).unwrap(),
"\"high\""
);
assert_eq!(
serde_json::from_str::<TaskPriority>("\"low\"").unwrap(),
TaskPriority::Low
);
assert_eq!(
serde_json::from_str::<TaskPriority>("\"medium\"").unwrap(),
TaskPriority::Medium
);
assert_eq!(
serde_json::from_str::<TaskPriority>("\"high\"").unwrap(),
TaskPriority::High
);
}
#[test]
fn test_task_status_default() {
assert_eq!(TaskStatus::default(), TaskStatus::Pending);
}
#[test]
fn test_task_priority_default() {
assert_eq!(TaskPriority::default(), TaskPriority::Medium);
}
#[test]
fn test_task_serialization_roundtrip() {
let task = Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Implement feature X".to_string(),
description: "Add the new feature X to the system".to_string(),
status: TaskStatus::InProgress,
priority: TaskPriority::High,
labels: vec!["feature".to_string(), "urgent".to_string()],
blocks: vec![TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAW")],
created_at: "2025-01-23T10:00:00Z".to_string(),
updated_at: "2025-01-23T11:00:00Z".to_string(),
created_by_session: Some("session-123".to_string()),
updated_by_session: Some("session-456".to_string()),
owner: None,
metadata: std::collections::HashMap::new(),
blocked_by: vec![],
};
let json = serde_json::to_string_pretty(&task).unwrap();
let parsed: Task = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.id, task.id);
assert_eq!(parsed.subject, task.subject);
assert_eq!(parsed.description, task.description);
assert_eq!(parsed.status, task.status);
assert_eq!(parsed.priority, task.priority);
assert_eq!(parsed.labels, task.labels);
assert_eq!(parsed.blocks, task.blocks);
assert_eq!(parsed.created_at, task.created_at);
assert_eq!(parsed.updated_at, task.updated_at);
assert_eq!(parsed.created_by_session, task.created_by_session);
assert_eq!(parsed.updated_by_session, task.updated_by_session);
}
#[test]
fn test_task_store_data_serde() {
let data = TaskStoreData {
meta: TaskStoreMeta {
version: 1,
project_id: "my-project".to_string(),
created_at: "2025-01-23T10:00:00Z".to_string(),
store_rev: 42,
},
tasks: vec![
Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Task 1".to_string(),
description: "Description 1".to_string(),
status: TaskStatus::Pending,
priority: TaskPriority::Medium,
labels: vec![],
blocks: vec![],
created_at: "2025-01-23T10:00:00Z".to_string(),
updated_at: "2025-01-23T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: None,
metadata: std::collections::HashMap::new(),
blocked_by: vec![],
},
Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAW"),
subject: "Task 2".to_string(),
description: "Description 2".to_string(),
status: TaskStatus::Completed,
priority: TaskPriority::Low,
labels: vec!["done".to_string()],
blocks: vec![],
created_at: "2025-01-23T11:00:00Z".to_string(),
updated_at: "2025-01-23T12:00:00Z".to_string(),
created_by_session: Some("session-1".to_string()),
updated_by_session: Some("session-2".to_string()),
owner: None,
metadata: std::collections::HashMap::new(),
blocked_by: vec![],
},
],
};
let json = serde_json::to_string_pretty(&data).unwrap();
let parsed: TaskStoreData = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.meta.version, data.meta.version);
assert_eq!(parsed.meta.project_id, data.meta.project_id);
assert_eq!(parsed.meta.created_at, data.meta.created_at);
assert_eq!(parsed.meta.store_rev, data.meta.store_rev);
assert_eq!(parsed.tasks.len(), 2);
assert_eq!(parsed.tasks[0].id, data.tasks[0].id);
assert_eq!(parsed.tasks[1].id, data.tasks[1].id);
}
#[test]
fn test_task_error_display() {
let err = TaskError::NotFound("123".to_string());
assert_eq!(err.to_string(), "Task not found: 123");
let err = TaskError::StorageError("disk full".to_string());
assert_eq!(err.to_string(), "Storage error: disk full");
let err = TaskError::InvalidData("missing subject".to_string());
assert_eq!(err.to_string(), "Invalid task data: missing subject");
}
#[test]
fn test_task_update_default() {
let update = TaskUpdate::default();
assert!(update.subject.is_none());
assert!(update.description.is_none());
assert!(update.status.is_none());
assert!(update.priority.is_none());
assert!(update.labels.is_none());
assert!(update.add_blocks.is_none());
assert!(update.remove_blocks.is_none());
}
#[test]
fn test_task_update_has_owner_field() {
let update = TaskUpdate {
owner: Some("alice".to_string()),
..Default::default()
};
assert_eq!(update.owner, Some("alice".to_string()));
let default_update = TaskUpdate::default();
assert!(default_update.owner.is_none());
}
#[test]
fn test_task_update_has_metadata_field() {
let mut metadata = std::collections::HashMap::new();
metadata.insert("key1".to_string(), serde_json::json!("value1"));
metadata.insert("key2".to_string(), serde_json::json!(42));
metadata.insert("key3".to_string(), serde_json::Value::Null);
let update = TaskUpdate {
metadata: Some(metadata.clone()),
..Default::default()
};
assert!(update.metadata.is_some());
let meta = update.metadata.unwrap();
assert_eq!(meta.get("key1"), Some(&serde_json::json!("value1")));
assert_eq!(meta.get("key2"), Some(&serde_json::json!(42)));
assert_eq!(meta.get("key3"), Some(&serde_json::Value::Null));
let default_update = TaskUpdate::default();
assert!(default_update.metadata.is_none());
}
#[test]
fn test_task_update_has_add_blocked_by_field() {
let update = TaskUpdate {
add_blocked_by: Some(vec![
TaskId::from_string("blocker-1"),
TaskId::from_string("blocker-2"),
]),
..Default::default()
};
assert!(update.add_blocked_by.is_some());
let blocked_by = update.add_blocked_by.unwrap();
assert_eq!(blocked_by.len(), 2);
assert_eq!(blocked_by[0], TaskId::from_string("blocker-1"));
assert_eq!(blocked_by[1], TaskId::from_string("blocker-2"));
let default_update = TaskUpdate::default();
assert!(default_update.add_blocked_by.is_none());
}
#[test]
fn test_task_update_has_remove_blocked_by_field() {
let update = TaskUpdate {
remove_blocked_by: Some(vec![TaskId::from_string("blocker-1")]),
..Default::default()
};
assert!(update.remove_blocked_by.is_some());
let remove_blocked_by = update.remove_blocked_by.unwrap();
assert_eq!(remove_blocked_by.len(), 1);
assert_eq!(remove_blocked_by[0], TaskId::from_string("blocker-1"));
let default_update = TaskUpdate::default();
assert!(default_update.remove_blocked_by.is_none());
}
#[test]
fn test_task_update_all_new_fields_together() {
let mut metadata = std::collections::HashMap::new();
metadata.insert(
"priority_reason".to_string(),
serde_json::json!("urgent customer request"),
);
let update = TaskUpdate {
owner: Some("bob".to_string()),
metadata: Some(metadata),
add_blocked_by: Some(vec![TaskId::from_string("prerequisite-task")]),
remove_blocked_by: Some(vec![TaskId::from_string("old-blocker")]),
..Default::default()
};
assert_eq!(update.owner, Some("bob".to_string()));
assert!(update.metadata.is_some());
assert!(update.add_blocked_by.is_some());
assert!(update.remove_blocked_by.is_some());
}
mod task_owner_tests {
use super::*;
#[test]
fn test_task_owner_field_exists() {
let task = Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Test task".to_string(),
description: "Test description".to_string(),
status: TaskStatus::Pending,
priority: TaskPriority::Medium,
labels: vec![],
blocks: vec![],
created_at: "2025-01-24T10:00:00Z".to_string(),
updated_at: "2025-01-24T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: None, metadata: std::collections::HashMap::new(),
blocked_by: vec![],
};
assert!(task.owner.is_none());
}
#[test]
fn test_task_owner_with_value() {
let task = Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Test task".to_string(),
description: "Test description".to_string(),
status: TaskStatus::InProgress,
priority: TaskPriority::High,
labels: vec![],
blocks: vec![],
created_at: "2025-01-24T10:00:00Z".to_string(),
updated_at: "2025-01-24T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: Some("alice".to_string()),
metadata: std::collections::HashMap::new(),
blocked_by: vec![],
};
assert_eq!(task.owner, Some("alice".to_string()));
}
#[test]
fn test_task_owner_serialization() {
let task = Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Test task".to_string(),
description: "Test".to_string(),
status: TaskStatus::Pending,
priority: TaskPriority::Medium,
labels: vec![],
blocks: vec![],
created_at: "2025-01-24T10:00:00Z".to_string(),
updated_at: "2025-01-24T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: Some("bob".to_string()),
metadata: std::collections::HashMap::new(),
blocked_by: vec![],
};
let json = serde_json::to_string(&task).unwrap();
assert!(json.contains("\"owner\":\"bob\""));
let parsed: Task = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.owner, Some("bob".to_string()));
}
#[test]
fn test_task_owner_none_serialization() {
let task = Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Test task".to_string(),
description: "Test".to_string(),
status: TaskStatus::Pending,
priority: TaskPriority::Medium,
labels: vec![],
blocks: vec![],
created_at: "2025-01-24T10:00:00Z".to_string(),
updated_at: "2025-01-24T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: None,
metadata: std::collections::HashMap::new(),
blocked_by: vec![],
};
let json = serde_json::to_string(&task).unwrap();
let parsed: Task = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.owner, None);
}
}
mod task_metadata_tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_task_metadata_field_exists() {
let task = Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Test task".to_string(),
description: "Test description".to_string(),
status: TaskStatus::Pending,
priority: TaskPriority::Medium,
labels: vec![],
blocks: vec![],
created_at: "2025-01-24T10:00:00Z".to_string(),
updated_at: "2025-01-24T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: None,
metadata: HashMap::new(),
blocked_by: vec![],
};
assert!(task.metadata.is_empty());
}
#[test]
fn test_task_metadata_with_values() {
let mut metadata = HashMap::new();
metadata.insert("priority_score".to_string(), serde_json::json!(42));
metadata.insert("estimate_hours".to_string(), serde_json::json!(8.5));
metadata.insert(
"assignee_email".to_string(),
serde_json::json!("alice@example.com"),
);
let task = Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Test task".to_string(),
description: "Test description".to_string(),
status: TaskStatus::Pending,
priority: TaskPriority::Medium,
labels: vec![],
blocks: vec![],
created_at: "2025-01-24T10:00:00Z".to_string(),
updated_at: "2025-01-24T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: None,
metadata,
blocked_by: vec![],
};
assert_eq!(task.metadata.len(), 3);
assert_eq!(
task.metadata.get("priority_score"),
Some(&serde_json::json!(42))
);
assert_eq!(
task.metadata.get("estimate_hours"),
Some(&serde_json::json!(8.5))
);
}
#[test]
fn test_task_metadata_with_complex_values() {
let mut metadata = HashMap::new();
metadata.insert(
"tags".to_string(),
serde_json::json!(["frontend", "urgent"]),
);
metadata.insert(
"config".to_string(),
serde_json::json!({"retries": 3, "timeout": 30}),
);
let task = Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Test task".to_string(),
description: "Test".to_string(),
status: TaskStatus::Pending,
priority: TaskPriority::Medium,
labels: vec![],
blocks: vec![],
created_at: "2025-01-24T10:00:00Z".to_string(),
updated_at: "2025-01-24T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: None,
metadata,
blocked_by: vec![],
};
let tags = task.metadata.get("tags").unwrap();
assert!(tags.is_array());
assert_eq!(tags.as_array().unwrap().len(), 2);
let config = task.metadata.get("config").unwrap();
assert!(config.is_object());
assert_eq!(config.get("retries"), Some(&serde_json::json!(3)));
}
#[test]
fn test_task_metadata_serialization() {
let mut metadata = HashMap::new();
metadata.insert("key1".to_string(), serde_json::json!("value1"));
metadata.insert("key2".to_string(), serde_json::json!(123));
let task = Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Test task".to_string(),
description: "Test".to_string(),
status: TaskStatus::Pending,
priority: TaskPriority::Medium,
labels: vec![],
blocks: vec![],
created_at: "2025-01-24T10:00:00Z".to_string(),
updated_at: "2025-01-24T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: None,
metadata,
blocked_by: vec![],
};
let json = serde_json::to_string(&task).unwrap();
assert!(json.contains("\"metadata\""));
let parsed: Task = serde_json::from_str(&json).unwrap();
assert_eq!(
parsed.metadata.get("key1"),
Some(&serde_json::json!("value1"))
);
assert_eq!(parsed.metadata.get("key2"), Some(&serde_json::json!(123)));
}
}
mod task_blocked_by_tests {
use super::*;
#[test]
fn test_task_blocked_by_field_exists() {
let task = Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Test task".to_string(),
description: "Test description".to_string(),
status: TaskStatus::Pending,
priority: TaskPriority::Medium,
labels: vec![],
blocks: vec![],
created_at: "2025-01-24T10:00:00Z".to_string(),
updated_at: "2025-01-24T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: None,
metadata: std::collections::HashMap::new(),
blocked_by: vec![],
};
assert!(task.blocked_by.is_empty());
}
#[test]
fn test_task_blocked_by_single_blocker() {
let blocker_id = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAW");
let task = Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Blocked task".to_string(),
description: "This task is blocked by another".to_string(),
status: TaskStatus::Pending,
priority: TaskPriority::Medium,
labels: vec![],
blocks: vec![],
created_at: "2025-01-24T10:00:00Z".to_string(),
updated_at: "2025-01-24T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: None,
metadata: std::collections::HashMap::new(),
blocked_by: vec![blocker_id.clone()],
};
assert_eq!(task.blocked_by.len(), 1);
assert_eq!(task.blocked_by[0], blocker_id);
}
#[test]
fn test_task_blocked_by_multiple_blockers() {
let blocker1 = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FA1");
let blocker2 = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FA2");
let blocker3 = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FA3");
let task = Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Multi-blocked task".to_string(),
description: "This task is blocked by three others".to_string(),
status: TaskStatus::Pending,
priority: TaskPriority::High,
labels: vec![],
blocks: vec![],
created_at: "2025-01-24T10:00:00Z".to_string(),
updated_at: "2025-01-24T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: None,
metadata: std::collections::HashMap::new(),
blocked_by: vec![blocker1.clone(), blocker2.clone(), blocker3.clone()],
};
assert_eq!(task.blocked_by.len(), 3);
assert!(task.blocked_by.contains(&blocker1));
assert!(task.blocked_by.contains(&blocker2));
assert!(task.blocked_by.contains(&blocker3));
}
#[test]
fn test_task_blocked_by_serialization() {
let blocker_id = TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAW");
let task = Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Blocked task".to_string(),
description: "Test".to_string(),
status: TaskStatus::Pending,
priority: TaskPriority::Medium,
labels: vec![],
blocks: vec![],
created_at: "2025-01-24T10:00:00Z".to_string(),
updated_at: "2025-01-24T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: None,
metadata: std::collections::HashMap::new(),
blocked_by: vec![blocker_id.clone()],
};
let json = serde_json::to_string(&task).unwrap();
assert!(json.contains("\"blocked_by\""));
assert!(json.contains("01ARZ3NDEKTSV4RRFFQ69G5FAW"));
let parsed: Task = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.blocked_by.len(), 1);
assert_eq!(parsed.blocked_by[0], blocker_id);
}
#[test]
fn test_task_blocks_vs_blocked_by_distinction() {
let task_a_id = TaskId::from_string("TASK_A_ID_00000000000000");
let task_b_id = TaskId::from_string("TASK_B_ID_00000000000000");
let task_a = Task {
id: task_a_id.clone(),
subject: "Task A - the blocker".to_string(),
description: "This task blocks Task B".to_string(),
status: TaskStatus::InProgress,
priority: TaskPriority::High,
labels: vec![],
blocks: vec![task_b_id.clone()], created_at: "2025-01-24T10:00:00Z".to_string(),
updated_at: "2025-01-24T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: None,
metadata: std::collections::HashMap::new(),
blocked_by: vec![], };
let task_b = Task {
id: task_b_id.clone(),
subject: "Task B - blocked".to_string(),
description: "This task is blocked by Task A".to_string(),
status: TaskStatus::Pending,
priority: TaskPriority::Medium,
labels: vec![],
blocks: vec![], created_at: "2025-01-24T10:00:00Z".to_string(),
updated_at: "2025-01-24T10:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
owner: None,
metadata: std::collections::HashMap::new(),
blocked_by: vec![task_a_id.clone()], };
assert!(task_a.blocks.contains(&task_b_id));
assert!(task_a.blocked_by.is_empty());
assert!(task_b.blocks.is_empty());
assert!(task_b.blocked_by.contains(&task_a_id));
}
}
}