#![allow(clippy::similar_names)]
mod common;
use beads_rust::model::{Comment, DependencyType, EventType, Issue, IssueType, Priority, Status};
use beads_rust::storage::{IssueUpdate, SqliteStorage};
use chrono::{Duration, Utc};
use common::{fixtures, test_db, test_db_with_dir};
#[test]
fn create_issue_minimal_fields() {
let mut storage = test_db();
let issue = fixtures::issue("minimal-create");
storage.create_issue(&issue, "tester").unwrap();
let retrieved = storage.get_issue(&issue.id).unwrap().expect("issue exists");
assert_eq!(retrieved.id, issue.id);
assert_eq!(retrieved.title, "minimal-create");
assert_eq!(retrieved.status, Status::Open);
assert_eq!(retrieved.priority, Priority::MEDIUM);
assert_eq!(retrieved.issue_type, IssueType::Task);
}
#[test]
fn create_issue_all_fields_populated() {
let mut storage = test_db();
let now = Utc::now();
let due_date = now + Duration::days(7);
let defer_date = now + Duration::days(1);
let issue = Issue {
id: "test-all-fields".to_string(),
title: "All Fields Issue".to_string(),
description: Some("Detailed description".to_string()),
design: Some("Technical design notes".to_string()),
acceptance_criteria: Some("Must pass all tests".to_string()),
notes: Some("Additional notes".to_string()),
status: Status::Open,
priority: Priority::HIGH,
issue_type: IssueType::Feature,
assignee: Some("alice".to_string()),
owner: Some("bob".to_string()),
estimated_minutes: Some(120),
created_at: now,
created_by: Some("creator".to_string()),
updated_at: now,
due_at: Some(due_date),
defer_until: Some(defer_date),
external_ref: Some("JIRA-123".to_string()),
ephemeral: false,
pinned: true,
is_template: false,
labels: vec![],
dependencies: vec![],
comments: vec![],
content_hash: None,
closed_at: None,
close_reason: None,
closed_by_session: None,
source_system: None,
source_repo: None,
source_repo_path: None,
agent_context: None,
deleted_at: None,
deleted_by: None,
delete_reason: None,
original_type: None,
compaction_level: None,
compacted_at: None,
compacted_at_commit: None,
original_size: None,
sender: None,
};
storage.create_issue(&issue, "tester").unwrap();
let retrieved = storage.get_issue(&issue.id).unwrap().expect("issue exists");
assert_eq!(retrieved.title, "All Fields Issue");
assert_eq!(
retrieved.description,
Some("Detailed description".to_string())
);
assert_eq!(retrieved.priority, Priority::HIGH);
assert_eq!(retrieved.issue_type, IssueType::Feature);
assert_eq!(retrieved.assignee, Some("alice".to_string()));
assert_eq!(retrieved.owner, Some("bob".to_string()));
assert_eq!(retrieved.estimated_minutes, Some(120));
assert_eq!(retrieved.external_ref, Some("JIRA-123".to_string()));
assert!(retrieved.pinned);
assert!(!retrieved.is_template);
assert!(retrieved.due_at.is_some());
assert!(retrieved.defer_until.is_some());
}
#[test]
fn create_issue_records_created_event() {
let mut storage = test_db();
let issue = fixtures::issue("event-create");
storage.create_issue(&issue, "event-actor").unwrap();
let details = storage
.get_issue_details(&issue.id, false, true, 100)
.unwrap()
.expect("issue exists");
assert!(!details.events.is_empty());
let created_event = details
.events
.iter()
.find(|e| e.event_type == EventType::Created);
assert!(created_event.is_some());
assert_eq!(created_event.unwrap().actor, "event-actor");
}
#[test]
fn create_issue_marks_dirty() {
let mut storage = test_db();
let issue = fixtures::issue("dirty-create");
storage.create_issue(&issue, "tester").unwrap();
let dirty_ids = storage.get_dirty_issue_ids().unwrap();
assert!(dirty_ids.contains(&issue.id));
}
#[test]
fn create_duplicate_id_fails() {
let mut storage = test_db();
let issue = fixtures::issue("duplicate-id");
storage.create_issue(&issue, "tester").unwrap();
let result = storage.create_issue(&issue, "tester");
assert!(result.is_err());
}
#[test]
fn get_issue_returns_none_for_nonexistent() {
let storage = test_db();
let result = storage.get_issue("nonexistent-id").unwrap();
assert!(result.is_none());
}
#[test]
fn get_issue_returns_issue_with_correct_types() {
let mut storage = test_db();
let mut issue = fixtures::issue("type-check");
issue.priority = Priority::CRITICAL;
issue.issue_type = IssueType::Bug;
issue.status = Status::InProgress;
storage.create_issue(&issue, "tester").unwrap();
let retrieved = storage.get_issue(&issue.id).unwrap().expect("issue exists");
assert_eq!(retrieved.priority, Priority::CRITICAL);
assert_eq!(retrieved.issue_type, IssueType::Bug);
assert_eq!(retrieved.status, Status::InProgress);
}
#[test]
fn get_issue_for_export_includes_relations() {
let mut storage = test_db();
let blocking_issue = fixtures::issue("export-blocker");
let blocked_issue = fixtures::issue("export-blocked");
storage.create_issue(&blocking_issue, "tester").unwrap();
storage.create_issue(&blocked_issue, "tester").unwrap();
storage
.add_dependency(
&blocked_issue.id,
&blocking_issue.id,
DependencyType::Blocks.as_str(),
"tester",
)
.unwrap();
storage
.add_label(&blocked_issue.id, "test-label", "tester")
.unwrap();
storage
.add_comment(&blocked_issue.id, "commenter", "Test comment")
.unwrap();
let exported = storage
.get_issue_for_export(&blocked_issue.id)
.unwrap()
.expect("issue exists");
assert!(!exported.dependencies.is_empty());
assert!(!exported.labels.is_empty());
assert!(!exported.comments.is_empty());
assert!(exported.labels.contains(&"test-label".to_string()));
}
#[test]
fn comments_with_same_timestamp_are_ordered_by_id() {
let mut storage = test_db();
let issue = fixtures::issue("same-timestamp-comments");
let created_at = Utc::now();
storage.create_issue(&issue, "tester").unwrap();
storage
.sync_comments_for_import(
&issue.id,
&[
Comment {
id: 20,
issue_id: issue.id.clone(),
author: "reviewer".to_string(),
body: "second by id".to_string(),
created_at,
},
Comment {
id: 10,
issue_id: issue.id.clone(),
author: "reviewer".to_string(),
body: "first by id".to_string(),
created_at,
},
],
)
.unwrap();
let comments = storage.get_comments(&issue.id).unwrap();
let ids = comments
.iter()
.map(|comment| comment.id)
.collect::<Vec<_>>();
assert_eq!(ids, vec![10, 20]);
let exported = storage
.get_issue_for_export(&issue.id)
.unwrap()
.expect("issue exists");
let exported_ids = exported
.comments
.iter()
.map(|comment| comment.id)
.collect::<Vec<_>>();
assert_eq!(exported_ids, vec![10, 20]);
}
#[test]
fn get_issue_details_includes_events_and_relations() {
let mut storage = test_db();
let issue = fixtures::issue("details-test");
storage.create_issue(&issue, "tester").unwrap();
storage
.add_label(&issue.id, "detail-label", "tester")
.unwrap();
let details = storage
.get_issue_details(&issue.id, true, true, 100)
.unwrap()
.expect("issue exists");
assert_eq!(details.issue.id, issue.id);
assert!(details.labels.contains(&"detail-label".to_string()));
assert!(!details.events.is_empty());
}
#[test]
fn update_issue_single_field_title() {
let mut storage = test_db();
let issue = fixtures::issue("update-title");
storage.create_issue(&issue, "tester").unwrap();
let update = IssueUpdate {
title: Some("Updated Title".to_string()),
..Default::default()
};
let updated = storage.update_issue(&issue.id, &update, "updater").unwrap();
assert_eq!(updated.title, "Updated Title");
let retrieved = storage.get_issue(&issue.id).unwrap().expect("issue exists");
assert_eq!(retrieved.title, "Updated Title");
}
#[test]
fn update_issue_multiple_fields() {
let mut storage = test_db();
let issue = fixtures::issue("update-multiple");
storage.create_issue(&issue, "tester").unwrap();
let update = IssueUpdate {
title: Some("New Title".to_string()),
description: Some(Some("New description".to_string())),
priority: Some(Priority::HIGH),
assignee: Some(Some("new-assignee".to_string())),
..Default::default()
};
let updated = storage.update_issue(&issue.id, &update, "updater").unwrap();
assert_eq!(updated.title, "New Title");
assert_eq!(updated.description, Some("New description".to_string()));
assert_eq!(updated.priority, Priority::HIGH);
assert_eq!(updated.assignee, Some("new-assignee".to_string()));
}
#[test]
fn update_issue_status_records_event() {
let mut storage = test_db();
let issue = fixtures::issue("update-status-event");
storage.create_issue(&issue, "tester").unwrap();
let update = IssueUpdate {
status: Some(Status::InProgress),
..Default::default()
};
storage.update_issue(&issue.id, &update, "updater").unwrap();
let details = storage
.get_issue_details(&issue.id, false, true, 100)
.unwrap()
.expect("issue exists");
let status_event = details
.events
.iter()
.find(|e| e.event_type == EventType::StatusChanged);
assert!(status_event.is_some());
let event = status_event.unwrap();
assert_eq!(event.old_value, Some("open".to_string()));
assert_eq!(event.new_value, Some("in_progress".to_string()));
}
#[test]
fn update_issue_priority_records_event() {
let mut storage = test_db();
let issue = fixtures::issue("update-priority-event");
storage.create_issue(&issue, "tester").unwrap();
let update = IssueUpdate {
priority: Some(Priority::CRITICAL),
..Default::default()
};
storage.update_issue(&issue.id, &update, "updater").unwrap();
let details = storage
.get_issue_details(&issue.id, false, true, 100)
.unwrap()
.expect("issue exists");
let priority_event = details
.events
.iter()
.find(|e| e.event_type == EventType::PriorityChanged);
assert!(priority_event.is_some());
}
#[test]
fn update_issue_empty_update_is_noop() {
let mut storage = test_db();
let issue = fixtures::issue("empty-update");
storage.create_issue(&issue, "tester").unwrap();
let dirty = storage.get_dirty_issue_ids().unwrap();
storage.clear_dirty_flags(&dirty).unwrap();
let update = IssueUpdate::default();
let updated = storage.update_issue(&issue.id, &update, "updater").unwrap();
assert_eq!(updated.title, issue.title);
let dirty_after = storage.get_dirty_issue_ids().unwrap();
assert!(dirty_after.is_empty());
}
#[test]
fn update_issue_marks_dirty() {
let mut storage = test_db();
let issue = fixtures::issue("dirty-update");
storage.create_issue(&issue, "tester").unwrap();
let dirty = storage.get_dirty_issue_ids().unwrap();
storage.clear_dirty_flags(&dirty).unwrap();
let update = IssueUpdate {
title: Some("Changed".to_string()),
..Default::default()
};
storage.update_issue(&issue.id, &update, "updater").unwrap();
let dirty_ids = storage.get_dirty_issue_ids().unwrap();
assert!(dirty_ids.contains(&issue.id));
}
#[test]
fn update_nonexistent_issue_fails() {
let mut storage = test_db();
let update = IssueUpdate {
title: Some("New Title".to_string()),
..Default::default()
};
let result = storage.update_issue("nonexistent", &update, "updater");
assert!(result.is_err());
}
#[test]
fn update_issue_rejects_empty_title_without_persisting() {
let mut storage = test_db();
let issue = fixtures::issue("reject-empty-title-update");
storage.create_issue(&issue, "tester").unwrap();
let update = IssueUpdate {
title: Some(" ".to_string()),
..Default::default()
};
let error = storage
.update_issue(&issue.id, &update, "updater")
.expect_err("empty updated title must fail validation");
assert!(error.to_string().contains("title"));
let retrieved = storage.get_issue(&issue.id).unwrap().expect("issue exists");
assert_eq!(retrieved.title, issue.title);
}
#[test]
fn update_issue_rejects_negative_estimate_without_persisting() {
let mut storage = test_db();
let issue = fixtures::issue("reject-negative-estimate-update");
storage.create_issue(&issue, "tester").unwrap();
let update = IssueUpdate {
estimated_minutes: Some(Some(-1)),
..Default::default()
};
let error = storage
.update_issue(&issue.id, &update, "updater")
.expect_err("negative updated estimate must fail validation");
assert!(error.to_string().contains("estimated_minutes"));
let retrieved = storage.get_issue(&issue.id).unwrap().expect("issue exists");
assert_eq!(retrieved.estimated_minutes, issue.estimated_minutes);
}
#[test]
fn update_issue_clear_optional_fields() {
let mut storage = test_db();
let issue = Issue {
id: "test-clear-fields".to_string(),
title: "Clear Fields Test".to_string(),
description: Some("Description".to_string()),
assignee: Some("alice".to_string()),
owner: Some("bob".to_string()),
estimated_minutes: Some(60),
status: Status::Open,
priority: Priority::MEDIUM,
issue_type: IssueType::Task,
created_at: Utc::now(),
updated_at: Utc::now(),
content_hash: None,
design: None,
acceptance_criteria: None,
notes: None,
created_by: None,
closed_at: None,
close_reason: None,
closed_by_session: None,
due_at: None,
defer_until: None,
external_ref: None,
source_system: None,
source_repo: None,
source_repo_path: None,
agent_context: None,
deleted_at: None,
deleted_by: None,
delete_reason: None,
original_type: None,
compaction_level: None,
compacted_at: None,
compacted_at_commit: None,
original_size: None,
sender: None,
ephemeral: false,
pinned: false,
is_template: false,
labels: vec![],
dependencies: vec![],
comments: vec![],
};
storage.create_issue(&issue, "tester").unwrap();
let update = IssueUpdate {
description: Some(None),
assignee: Some(None),
owner: Some(None),
estimated_minutes: Some(None),
..Default::default()
};
let updated = storage.update_issue(&issue.id, &update, "updater").unwrap();
assert!(updated.description.is_none());
assert!(updated.assignee.is_none());
assert!(updated.owner.is_none());
assert!(updated.estimated_minutes.is_none());
}
#[test]
fn delete_issue_creates_tombstone() {
let mut storage = test_db();
let issue = fixtures::issue("soft-delete");
storage.create_issue(&issue, "tester").unwrap();
storage
.delete_issue(&issue.id, "deleter", "No longer needed", None)
.unwrap();
let deleted = storage.get_issue(&issue.id).unwrap().expect("issue exists");
assert_eq!(deleted.status, Status::Tombstone);
assert!(deleted.deleted_at.is_some());
assert_eq!(deleted.deleted_by, Some("deleter".to_string()));
assert_eq!(deleted.delete_reason, Some("No longer needed".to_string()));
assert_eq!(deleted.original_type, Some("task".to_string()));
}
#[test]
fn delete_issue_records_event() {
let mut storage = test_db();
let issue = fixtures::issue("delete-event");
storage.create_issue(&issue, "tester").unwrap();
storage
.delete_issue(&issue.id, "deleter", "Test deletion", None)
.unwrap();
let details = storage
.get_issue_details(&issue.id, false, true, 100)
.unwrap()
.expect("issue exists");
let deleted_event = details
.events
.iter()
.find(|e| e.event_type == EventType::Deleted);
assert!(deleted_event.is_some());
assert_eq!(deleted_event.unwrap().actor, "deleter");
}
#[test]
fn delete_issue_marks_dirty() {
let mut storage = test_db();
let issue = fixtures::issue("dirty-delete");
storage.create_issue(&issue, "tester").unwrap();
let dirty = storage.get_dirty_issue_ids().unwrap();
storage.clear_dirty_flags(&dirty).unwrap();
storage
.delete_issue(&issue.id, "deleter", "Cleanup", None)
.unwrap();
let dirty_ids = storage.get_dirty_issue_ids().unwrap();
assert!(dirty_ids.contains(&issue.id));
}
#[test]
fn delete_nonexistent_issue_fails() {
let mut storage = test_db();
let result = storage.delete_issue("nonexistent", "deleter", "reason", None);
assert!(result.is_err());
}
#[test]
fn deleted_issues_excluded_from_list() {
let mut storage = test_db();
let issue1 = fixtures::issue("list-active");
let issue2 = fixtures::issue("list-deleted");
storage.create_issue(&issue1, "tester").unwrap();
storage.create_issue(&issue2, "tester").unwrap();
storage
.delete_issue(&issue2.id, "deleter", "removed", None)
.unwrap();
let filters = beads_rust::storage::ListFilters::default();
let listed = storage.list_issues(&filters).unwrap();
let ids: Vec<_> = listed.iter().map(|i| i.id.clone()).collect();
assert!(ids.contains(&issue1.id));
assert!(!ids.contains(&issue2.id));
}
#[test]
fn get_dirty_issue_ids_returns_all_dirty() {
let mut storage = test_db();
let issue1 = fixtures::issue("dirty-1");
let issue2 = fixtures::issue("dirty-2");
let issue3 = fixtures::issue("dirty-3");
storage.create_issue(&issue1, "tester").unwrap();
storage.create_issue(&issue2, "tester").unwrap();
storage.create_issue(&issue3, "tester").unwrap();
let dirty_ids = storage.get_dirty_issue_ids().unwrap();
assert_eq!(dirty_ids.len(), 3);
assert!(dirty_ids.contains(&issue1.id));
assert!(dirty_ids.contains(&issue2.id));
assert!(dirty_ids.contains(&issue3.id));
}
#[test]
fn clear_dirty_flags_removes_specified() {
let mut storage = test_db();
let issue1 = fixtures::issue("clear-dirty-1");
let issue2 = fixtures::issue("clear-dirty-2");
storage.create_issue(&issue1, "tester").unwrap();
storage.create_issue(&issue2, "tester").unwrap();
storage
.clear_dirty_flags(std::slice::from_ref(&issue1.id))
.unwrap();
let dirty_ids = storage.get_dirty_issue_ids().unwrap();
assert!(!dirty_ids.contains(&issue1.id));
assert!(dirty_ids.contains(&issue2.id));
}
#[test]
fn clear_dirty_flags_empty_list_is_noop() {
let mut storage = test_db();
let issue = fixtures::issue("clear-empty");
storage.create_issue(&issue, "tester").unwrap();
let count = storage.clear_dirty_flags(&[]).unwrap();
assert_eq!(count, 0);
let dirty_ids = storage.get_dirty_issue_ids().unwrap();
assert!(dirty_ids.contains(&issue.id));
}
#[test]
fn failed_create_does_not_persist() {
let mut storage = test_db();
let issue = fixtures::issue("transaction-test");
storage.create_issue(&issue, "tester").unwrap();
let result = storage.create_issue(&issue, "tester");
assert!(result.is_err());
let retrieved = storage.get_issue(&issue.id).unwrap().expect("issue exists");
assert_eq!(retrieved.title, issue.title);
let all_issues = storage
.list_issues(&beads_rust::storage::ListFilters::default())
.unwrap();
assert_eq!(all_issues.len(), 1);
}
#[test]
fn upsert_issue_for_import_creates_new() {
let storage = test_db();
let issue = fixtures::issue("upsert-new");
storage.upsert_issue_for_import(&issue).unwrap();
let retrieved = storage.get_issue(&issue.id).unwrap().expect("issue exists");
assert_eq!(retrieved.title, issue.title);
}
#[test]
fn upsert_issue_for_import_updates_existing() {
let mut storage = test_db();
let mut issue = fixtures::issue("upsert-existing");
storage.create_issue(&issue, "tester").unwrap();
issue.title = "Updated via upsert".to_string();
issue.description = Some("New description".to_string());
storage.upsert_issue_for_import(&issue).unwrap();
let retrieved = storage.get_issue(&issue.id).unwrap().expect("issue exists");
assert_eq!(retrieved.title, "Updated via upsert");
assert_eq!(retrieved.description, Some("New description".to_string()));
}
#[test]
fn dotted_issue_ids_upsert_lookup_and_update_exact_rows() {
let mut storage = test_db();
let mut parent = fixtures::issue("dotted parent");
parent.id = "bd-rchk0.5".to_string();
let mut target = fixtures::issue("dotted target");
target.id = "bd-rchk0.5.6".to_string();
let mut child = fixtures::issue("dotted child");
child.id = "bd-rchk0.5.6.1".to_string();
storage.upsert_issue_for_import(&parent).unwrap();
storage.upsert_issue_for_import(&target).unwrap();
storage.upsert_issue_for_import(&child).unwrap();
assert!(storage.id_exists("bd-rchk0.5").unwrap());
assert!(storage.id_exists("bd-rchk0.5.6").unwrap());
assert!(storage.id_exists("bd-rchk0.5.6.1").unwrap());
assert_eq!(
storage.find_ids_by_hash("rchk0.5.6").unwrap(),
vec!["bd-rchk0.5.6".to_string()]
);
let update = IssueUpdate {
title: Some("Updated dotted target".to_string()),
priority: Some(Priority::HIGH),
..IssueUpdate::default()
};
let updated = storage
.update_issue("bd-rchk0.5.6", &update, "tester")
.unwrap();
assert_eq!(updated.id, "bd-rchk0.5.6");
assert_eq!(updated.title, "Updated dotted target");
assert_eq!(updated.priority, Priority::HIGH);
assert_eq!(
storage
.get_issue("bd-rchk0.5")
.unwrap()
.expect("parent exists")
.title,
"dotted parent"
);
assert_eq!(
storage
.get_issue("bd-rchk0.5.6.1")
.unwrap()
.expect("child exists")
.title,
"dotted child"
);
}
#[test]
fn upsert_issue_stores_all_fields() {
let storage = test_db();
let now = Utc::now();
let issue = Issue {
id: "test-upsert-all".to_string(),
title: "Upsert All Fields".to_string(),
description: Some("Detailed description".to_string()),
design: Some("Technical design notes".to_string()),
acceptance_criteria: Some("Must pass all tests".to_string()),
notes: Some("Additional notes".to_string()),
status: Status::Open,
priority: Priority::HIGH,
issue_type: IssueType::Feature,
assignee: Some("alice".to_string()),
owner: Some("bob".to_string()),
estimated_minutes: Some(120),
created_at: now,
created_by: Some("creator".to_string()),
updated_at: now,
due_at: Some(now + Duration::days(7)),
defer_until: Some(now + Duration::days(1)),
external_ref: Some("JIRA-456".to_string()),
ephemeral: false,
pinned: true,
is_template: false,
labels: vec![],
dependencies: vec![],
comments: vec![],
content_hash: Some("abc123".to_string()),
closed_at: None,
close_reason: None,
closed_by_session: None,
source_system: Some("test".to_string()),
source_repo: None,
source_repo_path: None,
agent_context: None,
deleted_at: None,
deleted_by: None,
delete_reason: None,
original_type: None,
compaction_level: None,
compacted_at: None,
compacted_at_commit: None,
original_size: None,
sender: None,
};
storage.upsert_issue_for_import(&issue).unwrap();
let retrieved = storage.get_issue(&issue.id).unwrap().expect("issue exists");
assert_eq!(retrieved.title, "Upsert All Fields");
assert_eq!(
retrieved.description,
Some("Detailed description".to_string())
);
assert_eq!(retrieved.design, Some("Technical design notes".to_string()));
assert_eq!(
retrieved.acceptance_criteria,
Some("Must pass all tests".to_string())
);
assert_eq!(retrieved.notes, Some("Additional notes".to_string()));
assert_eq!(retrieved.priority, Priority::HIGH);
assert_eq!(retrieved.issue_type, IssueType::Feature);
assert_eq!(retrieved.assignee, Some("alice".to_string()));
assert_eq!(retrieved.owner, Some("bob".to_string()));
assert_eq!(retrieved.estimated_minutes, Some(120));
assert_eq!(retrieved.external_ref, Some("JIRA-456".to_string()));
assert_eq!(retrieved.content_hash, Some("abc123".to_string()));
assert_eq!(retrieved.source_system, Some("test".to_string()));
assert!(retrieved.pinned);
assert!(!retrieved.is_template);
}
#[test]
fn id_exists_returns_true_for_existing() {
let mut storage = test_db();
let issue = fixtures::issue("exists-test");
storage.create_issue(&issue, "tester").unwrap();
assert!(storage.id_exists(&issue.id).unwrap());
}
#[test]
fn id_exists_returns_false_for_nonexistent() {
let storage = test_db();
assert!(!storage.id_exists("nonexistent-id").unwrap());
}
#[test]
fn count_issues_returns_correct_count() {
let mut storage = test_db();
assert_eq!(storage.count_issues().unwrap(), 0);
let issue1 = fixtures::issue("count-1");
let issue2 = fixtures::issue("count-2");
let issue3 = fixtures::issue("count-3");
storage.create_issue(&issue1, "tester").unwrap();
assert_eq!(storage.count_issues().unwrap(), 1);
storage.create_issue(&issue2, "tester").unwrap();
storage.create_issue(&issue3, "tester").unwrap();
assert_eq!(storage.count_issues().unwrap(), 3);
}
#[test]
fn count_active_issues_excludes_closed_and_templates_but_includes_deferred() {
let mut storage = test_db();
let open = fixtures::issue("active-open");
let mut deferred = fixtures::issue("active-deferred");
deferred.status = Status::Deferred;
let mut closed = fixtures::issue("active-closed");
closed.status = Status::Closed;
closed.closed_at = Some(Utc::now());
let mut template = fixtures::issue("active-template");
template.is_template = true;
storage.create_issue(&open, "tester").unwrap();
storage.create_issue(&deferred, "tester").unwrap();
storage.create_issue(&closed, "tester").unwrap();
storage.create_issue(&template, "tester").unwrap();
assert_eq!(storage.count_issues().unwrap(), 4);
assert_eq!(storage.count_active_issues().unwrap(), 2);
}
#[test]
fn data_persists_across_connections() {
let (mut storage, dir) = test_db_with_dir();
let db_path = dir.path().join(".beads").join("beads.db");
let issue = fixtures::issue("persist-test");
storage.create_issue(&issue, "tester").unwrap();
drop(storage);
let storage2 = SqliteStorage::open(&db_path).unwrap();
let retrieved = storage2
.get_issue(&issue.id)
.unwrap()
.expect("issue exists");
assert_eq!(retrieved.title, issue.title);
}