use sift_queue::queue::{parse_priority_value, Item, Queue, Source, UpdateAttrs};
use std::collections::HashSet;
use tempfile::TempDir;
fn test_queue(dir: &TempDir) -> Queue {
let path = dir.path().join("queue.jsonl");
Queue::new(path)
}
#[test]
fn test_parse_minimal_item() {
let json = r#"{"id":"abc","status":"pending","sources":[{"type":"text","content":"Hello world"}],"metadata":{},"created_at":"2025-01-01T12:00:00.000Z","updated_at":"2025-01-01T12:00:00.000Z"}"#;
let item: Item = serde_json::from_str(json).unwrap();
assert_eq!(item.id, "abc");
assert_eq!(item.status, "pending");
assert!(item.title.is_none());
assert!(item.description.is_none());
assert_eq!(item.sources.len(), 1);
assert_eq!(item.sources[0].type_, "text");
assert_eq!(item.sources[0].content.as_deref(), Some("Hello world"));
assert!(item.blocked_by.is_empty());
assert!(item.errors.is_empty());
let serialized = item.to_json_value();
assert_eq!(serialized["id"], "abc");
assert_eq!(serialized["status"], "pending");
assert!(serialized.get("created_at").is_some());
assert!(serialized.get("updated_at").is_some());
}
#[test]
fn test_parse_full_item() {
let json = r#"{"id":"x1y","title":"Fix login bug","status":"in_progress","priority":1,"sources":[{"type":"diff","path":"/changes.patch"},{"type":"text","content":"Summary"}],"metadata":{"workflow":"analyze"},"created_at":"2025-01-01T12:00:00.000Z","updated_at":"2025-01-01T12:05:00.000Z","blocked_by":["abc","def"],"errors":[{"message":"timeout","timestamp":"2025-01-01T12:01:00.000Z"}]}"#;
let item: Item = serde_json::from_str(json).unwrap();
assert_eq!(item.id, "x1y");
assert_eq!(item.title.as_deref(), Some("Fix login bug"));
assert!(item.description.is_none());
assert_eq!(item.status, "in_progress");
assert_eq!(item.priority, Some(1));
assert_eq!(item.sources.len(), 2);
assert_eq!(item.blocked_by, vec!["abc", "def"]);
assert_eq!(item.errors.len(), 1);
let serialized = item.to_json_value();
assert_eq!(serialized["priority"], 1);
assert_eq!(serialized["blocked_by"], serde_json::json!(["abc", "def"]));
assert_eq!(serialized["errors"].as_array().unwrap().len(), 1);
}
#[test]
fn test_parse_priority_value_accepts_numeric_only() {
assert_eq!(parse_priority_value("0").unwrap(), 0);
assert_eq!(parse_priority_value("4").unwrap(), 4);
assert!(parse_priority_value("P4").is_err());
assert!(parse_priority_value("p2").is_err());
assert!(parse_priority_value("5").is_err());
}
#[test]
fn test_source_serialization() {
let json = r#"{"type":"directory","path":"/some/dir"}"#;
let source: Source = serde_json::from_str(json).unwrap();
assert_eq!(source.type_, "directory");
assert_eq!(source.path.as_deref(), Some("/some/dir"));
assert!(source.content.is_none());
let serialized = source.to_json_value();
assert_eq!(serialized["type"], "directory");
assert_eq!(serialized["path"], "/some/dir");
}
#[test]
fn test_unknown_source_type_serialization() {
let json = r#"{"type":"unknown","path":"/tmp/source"}"#;
let source: Source = serde_json::from_str(json).unwrap();
assert_eq!(source.type_, "unknown");
let serialized = source.to_json_value();
assert_eq!(serialized["type"], "unknown");
assert_eq!(serialized["path"], "/tmp/source");
}
#[test]
fn test_optional_fields_omitted_when_empty() {
let item = Item {
id: "abc".to_string(),
title: None,
description: None,
status: "pending".to_string(),
priority: None,
sources: vec![Source {
type_: "text".to_string(),
path: None,
content: Some("test".to_string()),
}],
metadata: serde_json::json!({}),
blocked_by: Vec::new(),
errors: Vec::new(),
created_at: "2025-01-01T12:00:00.000Z".to_string(),
updated_at: "2025-01-01T12:00:00.000Z".to_string(),
};
let json = item.to_json_string();
assert!(
!json.contains("\"title\""),
"title should be omitted when None"
);
assert!(
!json.contains("\"description\""),
"description should be omitted when None"
);
assert!(
!json.contains("\"blocked_by\""),
"blocked_by should be omitted when empty"
);
assert!(
!json.contains("\"errors\""),
"errors should be omitted when empty"
);
}
#[test]
fn test_push_and_find() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let item = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("Hello".to_string()),
}],
Some("Test".to_string()),
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
assert_eq!(item.id.len(), 3);
assert_eq!(item.status, "pending");
assert_eq!(item.title.as_deref(), Some("Test"));
let found = queue.find(&item.id);
assert!(found.is_some());
assert_eq!(found.unwrap().id, item.id);
}
#[test]
fn test_push_requires_some_content_when_sources_empty() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let result = queue.push(vec![], None, None, None, serde_json::json!({}), vec![]);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Item requires at least one source, title, or description"));
}
#[test]
fn test_push_with_title_allows_empty_sources() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let item = queue
.push(
vec![],
Some("Title".to_string()),
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
assert_eq!(item.title.as_deref(), Some("Title"));
assert!(item.sources.is_empty());
}
#[test]
fn test_push_allows_description_with_empty_sources() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let item = queue
.push(
vec![],
Some("Title".to_string()),
Some("Description".to_string()),
None,
serde_json::json!({}),
vec![],
)
.unwrap();
assert_eq!(item.title.as_deref(), Some("Title"));
assert_eq!(item.description.as_deref(), Some("Description"));
assert!(item.sources.is_empty());
}
#[test]
fn test_push_with_title_and_no_description_allows_empty_sources() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let item = queue
.push(
vec![],
Some("Title".to_string()),
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
assert_eq!(item.title.as_deref(), Some("Title"));
assert!(item.description.is_none());
assert!(item.sources.is_empty());
}
#[test]
fn test_push_with_metadata_only_requires_task_content() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let result = queue.push(
vec![],
None,
None,
None,
serde_json::json!({"kind":"task"}),
vec![],
);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Item requires at least one source, title, or description"));
}
#[test]
fn test_push_requires_some_content_when_description_and_sources_are_empty() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let result = queue.push(vec![], None, None, None, serde_json::json!({}), vec![]);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Item requires at least one source, title, or description"));
}
#[test]
fn test_push_with_priority_only_requires_task_content() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let result = queue.push(vec![], None, None, Some(1), serde_json::json!({}), vec![]);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Item requires at least one source, title, or description"));
}
#[test]
fn test_push_validates_source_type() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let result = queue.push(
vec![Source {
type_: "invalid".to_string(),
path: None,
content: Some("test".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid source type"));
}
#[test]
fn test_push_unique_ids() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let mut ids = HashSet::new();
for _ in 0..20 {
let item = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("test".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
assert!(
ids.insert(item.id.clone()),
"Duplicate ID generated: {}",
item.id
);
}
}
#[test]
fn test_all_items() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("first".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("second".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
let items = queue.all();
assert_eq!(items.len(), 2);
}
#[test]
fn test_all_nonexistent_file() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("nonexistent.jsonl");
let queue = Queue::new(path);
let items = queue.all();
assert!(items.is_empty());
}
#[test]
fn test_filter_by_status() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let item = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("test".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
queue
.update(
&item.id,
UpdateAttrs {
status: Some("closed".to_string()),
..Default::default()
},
)
.unwrap();
queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("test2".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
let pending = queue.filter(Some("pending"));
assert_eq!(pending.len(), 1);
let closed = queue.filter(Some("closed"));
assert_eq!(closed.len(), 1);
let all = queue.filter(None);
assert_eq!(all.len(), 2);
}
#[test]
fn test_ready_items() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let blocker = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("blocker".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
let _blocked = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("blocked".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![blocker.id.clone()],
)
.unwrap();
let ready = queue.ready();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].id, blocker.id);
}
#[test]
fn test_ready_in_progress_blocker_blocks_readiness() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let blocker = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("blocker".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
let _blocked = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("blocked".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![blocker.id.clone()],
)
.unwrap();
queue
.update(
&blocker.id,
UpdateAttrs {
status: Some("in_progress".to_string()),
..Default::default()
},
)
.unwrap();
let ready = queue.ready();
assert!(ready.is_empty());
}
#[test]
fn test_computed_status_reports_blocked_when_blocker_is_open() {
let blocker_id = "abc".to_string();
let item = Item {
id: "def".to_string(),
title: None,
description: None,
status: "pending".to_string(),
priority: None,
sources: vec![Source {
type_: "text".to_string(),
path: None,
content: Some("blocked".to_string()),
}],
metadata: serde_json::json!({}),
blocked_by: vec![blocker_id.clone()],
errors: Vec::new(),
created_at: "2025-01-01T12:00:00.000Z".to_string(),
updated_at: "2025-01-01T12:00:00.000Z".to_string(),
};
let open_ids = HashSet::from([blocker_id]);
assert_eq!(item.computed_status(Some(&open_ids)), "blocked");
}
#[test]
fn test_computed_status_reports_pending_when_blockers_are_closed() {
let item = Item {
id: "def".to_string(),
title: None,
description: None,
status: "pending".to_string(),
priority: None,
sources: vec![Source {
type_: "text".to_string(),
path: None,
content: Some("blocked".to_string()),
}],
metadata: serde_json::json!({}),
blocked_by: vec!["abc".to_string()],
errors: Vec::new(),
created_at: "2025-01-01T12:00:00.000Z".to_string(),
updated_at: "2025-01-01T12:00:00.000Z".to_string(),
};
let open_ids = HashSet::new();
assert_eq!(item.computed_status(Some(&open_ids)), "pending");
}
#[test]
fn test_find_with_computed_status_surfaces_blocked_item() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let blocker = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("blocker".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
let blocked = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("blocked".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![blocker.id.clone()],
)
.unwrap();
let found = queue.find_with_computed_status(&blocked.id).unwrap();
assert_eq!(found.status, "blocked");
}
#[test]
fn test_all_with_computed_status_surfaces_blocked_items() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let blocker = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("blocker".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
let blocked = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("blocked".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![blocker.id.clone()],
)
.unwrap();
let items = queue.all_with_computed_status();
let blocked = items.iter().find(|item| item.id == blocked.id).unwrap();
assert_eq!(blocked.status, "blocked");
}
#[test]
fn test_ready_unblocks_after_close() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let blocker = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("blocker".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
let blocked = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("blocked".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![blocker.id.clone()],
)
.unwrap();
queue
.update(
&blocker.id,
UpdateAttrs {
status: Some("closed".to_string()),
..Default::default()
},
)
.unwrap();
let ready = queue.ready();
assert_eq!(ready.len(), 1);
assert_eq!(ready[0].id, blocked.id);
}
#[test]
fn test_update_item() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let item = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("test".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
let updated = queue
.update(
&item.id,
UpdateAttrs {
status: Some("in_progress".to_string()),
title: Some("New title".to_string()),
description: Some("New description".to_string()),
priority: Some(Some(1)),
metadata: Some(serde_json::json!({"key": "value"})),
..Default::default()
},
)
.unwrap()
.unwrap();
assert_eq!(updated.status, "in_progress");
assert_eq!(updated.title.as_deref(), Some("New title"));
assert_eq!(updated.description.as_deref(), Some("New description"));
assert_eq!(updated.priority, Some(1));
assert_eq!(updated.metadata["key"], "value");
assert!(updated.updated_at >= item.updated_at);
}
#[test]
fn test_update_can_clear_priority() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let item = queue
.push(
vec![],
Some("Test".to_string()),
None,
Some(1),
serde_json::json!({}),
vec![],
)
.unwrap();
let updated = queue
.update(
&item.id,
UpdateAttrs {
priority: Some(None),
..Default::default()
},
)
.unwrap()
.unwrap();
assert_eq!(updated.priority, None);
}
#[test]
fn test_update_noop_preserves_updated_at() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let item = queue
.push(
vec![],
Some("Test".to_string()),
Some("Description".to_string()),
Some(1),
serde_json::json!({"kind":"task"}),
vec!["abc".to_string()],
)
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(5));
let updated = queue
.update(
&item.id,
UpdateAttrs {
status: Some(item.status.clone()),
title: item.title.clone(),
description: item.description.clone(),
priority: Some(item.priority),
metadata: Some(item.metadata.clone()),
blocked_by: Some(item.blocked_by.clone()),
sources: Some(item.sources.clone()),
},
)
.unwrap()
.unwrap();
assert_eq!(updated.updated_at, item.updated_at);
}
#[test]
fn test_update_invalid_status() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let item = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("test".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
let result = queue.update(
&item.id,
UpdateAttrs {
status: Some("invalid".to_string()),
..Default::default()
},
);
assert!(result.is_err());
}
#[test]
fn test_update_invalid_source_type() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let item = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("test".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
let result = queue.update(
&item.id,
UpdateAttrs {
sources: Some(vec![Source {
type_: "bogus".to_string(),
path: None,
content: Some("updated".to_string()),
}]),
..Default::default()
},
);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid source type"));
}
#[test]
fn test_update_rejects_self_blocked_item() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let item = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("test".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
let result = queue.update(
&item.id,
UpdateAttrs {
blocked_by: Some(vec![item.id.clone()]),
..Default::default()
},
);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("cannot block itself"));
}
#[test]
fn test_update_nonexistent() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let result = queue
.update(
"zzz",
UpdateAttrs {
status: Some("closed".to_string()),
..Default::default()
},
)
.unwrap();
assert!(result.is_none());
}
#[test]
fn test_remove_item() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let item = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("test".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
let removed = queue.remove(&item.id).unwrap();
assert!(removed.is_some());
assert_eq!(removed.unwrap().id, item.id);
let items = queue.all();
assert!(items.is_empty());
}
#[test]
fn test_remove_nonexistent() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let result = queue.remove("zzz").unwrap();
assert!(result.is_none());
}
#[test]
fn test_corrupt_line_skipped() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("queue.jsonl");
std::fs::write(
&path,
r#"{"id":"abc","status":"pending","sources":[{"type":"text","content":"good"}],"metadata":{},"created_at":"2025-01-01T12:00:00.000Z","updated_at":"2025-01-01T12:00:00.000Z"}
this is not valid json
{"id":"def","status":"pending","sources":[{"type":"text","content":"also good"}],"metadata":{},"created_at":"2025-01-01T12:00:00.000Z","updated_at":"2025-01-01T12:00:00.000Z"}
"#,
)
.unwrap();
let queue = Queue::new(path);
let items = queue.all();
assert_eq!(items.len(), 2);
assert_eq!(items[0].id, "abc");
assert_eq!(items[1].id, "def");
}
#[test]
fn test_empty_lines_skipped() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("queue.jsonl");
std::fs::write(
&path,
r#"
{"id":"abc","status":"pending","sources":[{"type":"text","content":"test"}],"metadata":{},"created_at":"2025-01-01T12:00:00.000Z","updated_at":"2025-01-01T12:00:00.000Z"}
"#,
)
.unwrap();
let queue = Queue::new(path);
let items = queue.all();
assert_eq!(items.len(), 1);
}
#[test]
fn test_timestamp_format() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let item = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("test".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
let re = regex_lite(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$");
assert!(
re.is_match(&item.created_at),
"Bad timestamp format: {}",
item.created_at
);
assert!(
re.is_match(&item.updated_at),
"Bad timestamp format: {}",
item.updated_at
);
}
fn regex_lite(pattern: &str) -> RegexLite {
RegexLite(pattern.to_string())
}
struct RegexLite(String);
impl RegexLite {
fn is_match(&self, s: &str) -> bool {
if self.0 == r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$" {
if s.len() != 24 {
return false;
}
let chars: Vec<char> = s.chars().collect();
chars[4] == '-'
&& chars[7] == '-'
&& chars[10] == 'T'
&& chars[13] == ':'
&& chars[16] == ':'
&& chars[19] == '.'
&& chars[23] == 'Z'
&& chars.iter().enumerate().all(|(i, c)| {
if [4, 7, 10, 13, 16, 19, 23].contains(&i) {
true
} else {
c.is_ascii_digit()
}
})
} else {
false
}
}
}
#[test]
fn test_id_format() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
for _ in 0..10 {
let item = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("test".to_string()),
}],
None,
None,
None,
serde_json::json!({}),
vec![],
)
.unwrap();
assert_eq!(item.id.len(), 3);
assert!(
item.id
.chars()
.all(|c: char| c.is_ascii_lowercase() || c.is_ascii_digit()),
"ID contains invalid chars: {}",
item.id
);
}
}
#[test]
fn test_metadata_preserved() {
let dir = TempDir::new().unwrap();
let queue = test_queue(&dir);
let item = queue
.push(
vec![Source {
type_: "text".to_string(),
path: None,
content: Some("test".to_string()),
}],
None,
None,
None,
serde_json::json!({"nested": {"key": "value"}, "array": [1, 2, 3]}),
vec![],
)
.unwrap();
let found = queue.find(&item.id).unwrap();
assert_eq!(found.metadata["nested"]["key"], "value");
assert_eq!(found.metadata["array"], serde_json::json!([1, 2, 3]));
}
#[test]
fn test_fixture_items_parse() {
let fixture = include_str!("fixtures/queue_samples.jsonl");
let items: Vec<Item> = fixture
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| serde_json::from_str(line).unwrap())
.collect();
assert_eq!(items.len(), 3);
assert_eq!(items[0].id, "abc");
assert_eq!(items[1].id, "x1y");
assert_eq!(items[2].id, "z99");
let full_item = &items[1];
assert_eq!(full_item.priority, Some(1));
assert_eq!(
full_item.metadata,
serde_json::json!({"workflow":"analyze"})
);
}