use chrono::Utc;
use jjj::db::{self, Database};
use jjj::models::{Confidence, Priority, Problem, ProblemStatus, Solution, SolutionStatus};
#[test]
fn test_full_sync_cycle() {
let db = Database::open_in_memory().unwrap();
let problem = Problem {
id: "p1".to_string(),
title: "Test problem".to_string(),
status: ProblemStatus::Open,
priority: Priority::High,
confidence: Confidence::default(),
parent_id: None,
milestone_id: None,
assignee: Some("alice".to_string()),
created_at: Utc::now(),
updated_at: Utc::now(),
description: "Test description".to_string(),
dissolved_reason: None,
solution_ids: vec![],
child_ids: vec![],
github_issue: None,
tags: vec![],
};
db::entities::upsert_problem(db.conn(), &problem).unwrap();
let solution = Solution {
id: "s1".to_string(),
title: "Test solution".to_string(),
status: SolutionStatus::Proposed,
problem_id: "p1".to_string(),
change_ids: vec!["abc123".to_string()],
supersedes: None,
assignee: None,
force_approved: false,
created_at: Utc::now(),
updated_at: Utc::now(),
approach: "Do the thing".to_string(),
critique_ids: vec![],
github_pr: None,
github_branch: None,
tags: vec![],
};
db::entities::upsert_solution(db.conn(), &solution).unwrap();
let errors = db::validate(&db).unwrap();
assert!(
errors.is_empty(),
"Expected no validation errors, got: {:?}",
errors
);
db.conn()
.execute(
"INSERT INTO fts (entity_type, entity_id, title, body) VALUES (?, ?, ?, ?)",
rusqlite::params!["problem", "p1", "Test problem", "Test description"],
)
.unwrap();
let results = db::search(db.conn(), "test", None).unwrap();
assert_eq!(
results.len(),
1,
"Expected 1 search result, got {}",
results.len()
);
assert_eq!(results[0].entity_id, "p1");
assert_eq!(results[0].entity_type, "problem");
}
#[test]
fn test_validation_catches_invalid_refs() {
let db = Database::open_in_memory().unwrap();
db.conn().execute("PRAGMA foreign_keys = OFF", []).unwrap();
let solution = Solution {
id: "s1".to_string(),
title: "Bad solution".to_string(),
status: SolutionStatus::Proposed,
problem_id: "p_invalid".to_string(), change_ids: vec![],
supersedes: None,
assignee: None,
force_approved: false,
created_at: Utc::now(),
updated_at: Utc::now(),
approach: "".to_string(),
critique_ids: vec![],
github_pr: None,
github_branch: None,
tags: vec![],
};
db::entities::upsert_solution(db.conn(), &solution).unwrap();
let errors = db::validate(&db).unwrap();
assert_eq!(
errors.len(),
1,
"Expected 1 validation error, got {:?}",
errors
);
assert!(
errors[0].message.contains("p_invalid"),
"Error message should mention the invalid reference: {}",
errors[0].message
);
assert!(
errors[0].message.contains("non-existent problem"),
"Error message should indicate non-existent problem: {}",
errors[0].message
);
}
#[test]
fn test_dirty_flag() {
let db = Database::open_in_memory().unwrap();
assert!(
!db::is_dirty(&db).unwrap(),
"Database should not be dirty initially"
);
db::set_dirty(&db, true).unwrap();
assert!(
db::is_dirty(&db).unwrap(),
"Database should be dirty after setting flag"
);
db::set_dirty(&db, false).unwrap();
assert!(
!db::is_dirty(&db).unwrap(),
"Database should not be dirty after clearing flag"
);
}
#[test]
fn test_problem_roundtrip() {
let db = Database::open_in_memory().unwrap();
let now = Utc::now();
let problem = Problem {
id: "p1".to_string(),
title: "Complex problem".to_string(),
status: ProblemStatus::InProgress,
priority: Priority::Critical,
confidence: Confidence::default(),
parent_id: None,
milestone_id: None,
assignee: Some("bob".to_string()),
created_at: now,
updated_at: now,
description: "Detailed description of the problem".to_string(),
dissolved_reason: None,
solution_ids: vec!["s1".to_string(), "s2".to_string()],
child_ids: vec!["p2".to_string()],
github_issue: None,
tags: vec![],
};
db::entities::upsert_problem(db.conn(), &problem).unwrap();
let loaded = db::entities::load_problem(db.conn(), "p1")
.unwrap()
.expect("Problem should exist");
assert_eq!(loaded.id, "p1");
assert_eq!(loaded.title, "Complex problem");
assert_eq!(loaded.status, ProblemStatus::InProgress);
assert_eq!(loaded.priority, Priority::Critical);
assert_eq!(loaded.assignee, Some("bob".to_string()));
assert_eq!(loaded.description, "Detailed description of the problem");
}
#[test]
fn test_search_with_entity_type_filter() {
let db = Database::open_in_memory().unwrap();
let problem = Problem::new("p1".to_string(), "Authentication bug".to_string());
db::entities::upsert_problem(db.conn(), &problem).unwrap();
let solution = Solution::new(
"s1".to_string(),
"Fix authentication flow".to_string(),
"p1".to_string(),
);
db::entities::upsert_solution(db.conn(), &solution).unwrap();
db.conn()
.execute(
"INSERT INTO fts (entity_type, entity_id, title, body) VALUES (?, ?, ?, ?)",
rusqlite::params!["problem", "p1", "Authentication bug", ""],
)
.unwrap();
db.conn()
.execute(
"INSERT INTO fts (entity_type, entity_id, title, body) VALUES (?, ?, ?, ?)",
rusqlite::params!["solution", "s1", "Fix authentication flow", ""],
)
.unwrap();
let results = db::search(db.conn(), "authentication", None).unwrap();
assert_eq!(results.len(), 2, "Expected 2 results without filter");
let results = db::search(db.conn(), "authentication", Some("problem")).unwrap();
assert_eq!(results.len(), 1, "Expected 1 result with problem filter");
assert_eq!(results[0].entity_type, "problem");
let results = db::search(db.conn(), "authentication", Some("solution")).unwrap();
assert_eq!(results.len(), 1, "Expected 1 result with solution filter");
assert_eq!(results[0].entity_type, "solution");
}
#[test]
fn test_valid_problem_hierarchy() {
let db = Database::open_in_memory().unwrap();
let parent = Problem::new("p1".to_string(), "Parent problem".to_string());
db::entities::upsert_problem(db.conn(), &parent).unwrap();
let mut child = Problem::new("p2".to_string(), "Child problem".to_string());
child.parent_id = Some("p1".to_string());
db::entities::upsert_problem(db.conn(), &child).unwrap();
let errors = db::validate(&db).unwrap();
assert!(
errors.is_empty(),
"Expected no validation errors for valid hierarchy, got: {:?}",
errors
);
}
#[test]
fn test_entity_deletion() {
let db = Database::open_in_memory().unwrap();
let problem = Problem::new("p1".to_string(), "To be deleted".to_string());
db::entities::upsert_problem(db.conn(), &problem).unwrap();
let loaded = db::entities::load_problem(db.conn(), "p1").unwrap();
assert!(loaded.is_some(), "Problem should exist before deletion");
let deleted = db::entities::delete_problem(db.conn(), "p1").unwrap();
assert!(deleted, "Delete should return true when row was deleted");
let loaded = db::entities::load_problem(db.conn(), "p1").unwrap();
assert!(loaded.is_none(), "Problem should not exist after deletion");
let deleted = db::entities::delete_problem(db.conn(), "p1").unwrap();
assert!(!deleted, "Delete should return false when no row exists");
}
#[test]
fn test_list_solutions_for_problem() {
let db = Database::open_in_memory().unwrap();
let p1 = Problem::new("p1".to_string(), "Problem 1".to_string());
let p2 = Problem::new("p2".to_string(), "Problem 2".to_string());
db::entities::upsert_problem(db.conn(), &p1).unwrap();
db::entities::upsert_problem(db.conn(), &p2).unwrap();
let s1 = Solution::new("s1".to_string(), "Solution 1".to_string(), "p1".to_string());
let s2 = Solution::new("s2".to_string(), "Solution 2".to_string(), "p1".to_string());
db::entities::upsert_solution(db.conn(), &s1).unwrap();
db::entities::upsert_solution(db.conn(), &s2).unwrap();
let s3 = Solution::new("s3".to_string(), "Solution 3".to_string(), "p2".to_string());
db::entities::upsert_solution(db.conn(), &s3).unwrap();
let solutions = db::entities::list_solutions_for_problem(db.conn(), "p1").unwrap();
assert_eq!(solutions.len(), 2, "Expected 2 solutions for p1");
let solutions = db::entities::list_solutions_for_problem(db.conn(), "p2").unwrap();
assert_eq!(solutions.len(), 1, "Expected 1 solution for p2");
assert_eq!(solutions[0].id, "s3");
}