//! Integration tests for the Tauri command layer. //! //! These tests exercise `AppState` and the command logic end-to-end using a //! real `Brittle` repository in a temp directory. Tauri IPC is not //! involved — the functions under test are plain Rust. use brittle_app::state::AppState; use brittle_core::{Brittle, EntryType, Person}; fn open_state() -> (AppState, tempfile::TempDir) { let tmp = tempfile::tempdir().unwrap(); let state = AppState::new(); let brittle = Brittle::create(tmp.path()).unwrap(); *state.brittle.lock().unwrap() = Some(brittle); (state, tmp) } // ── AppState ───────────────────────────────────────────────────────────────── #[test] fn no_repo_open_returns_error() { let state = AppState::new(); let err = state.with_repo(|_| Ok(())).unwrap_err(); assert_eq!(err, "no repository open"); } #[test] fn with_repo_propagates_brittle_errors() { let (state, _tmp) = open_state(); // Trying to get a non-existent reference propagates the StoreError. let err = state .with_repo_read(|b| { use brittle_core::model::ids::ReferenceId; b.get_reference(ReferenceId::new()) }) .unwrap_err(); assert!(!err.is_empty()); } // ── Repository lifecycle ────────────────────────────────────────────────────── #[test] fn create_and_reopen_repository() { let tmp = tempfile::tempdir().unwrap(); let state = AppState::new(); // Create { let brittle = Brittle::create(tmp.path()).unwrap(); *state.brittle.lock().unwrap() = Some(brittle); } // Close *state.brittle.lock().unwrap() = None; assert!(state.with_repo_read(|_| Ok(())).is_err()); // Reopen { let brittle = Brittle::open(tmp.path()).unwrap(); *state.brittle.lock().unwrap() = Some(brittle); } assert!(state.with_repo_read(|_| Ok(())).is_ok()); } // ── Reference CRUD ─────────────────────────────────────────────────────────── #[test] fn create_and_list_references() { let (state, _tmp) = open_state(); state .with_repo(|b| b.create_reference("turing1950", EntryType::Article)) .unwrap(); state .with_repo(|b| b.create_reference("knuth1984", EntryType::Book)) .unwrap(); let refs = state.with_repo_read(|b| b.list_references()).unwrap(); assert_eq!(refs.len(), 2); let keys: Vec<&str> = refs.iter().map(|r| r.cite_key.as_str()).collect(); assert!(keys.contains(&"turing1950")); assert!(keys.contains(&"knuth1984")); } #[test] fn delete_reference_removes_it() { let (state, _tmp) = open_state(); let r = state .with_repo(|b| b.create_reference("gone2024", EntryType::Misc)) .unwrap(); state.with_repo(|b| b.delete_reference(r.id)).unwrap(); let refs = state.with_repo_read(|b| b.list_references()).unwrap(); assert!(refs.is_empty()); } #[test] fn set_and_remove_field() { let (state, _tmp) = open_state(); let r = state .with_repo(|b| b.create_reference("fields2024", EntryType::Article)) .unwrap(); state .with_repo(|b| b.set_field(r.id, "title", "A Test Title")) .unwrap(); let fetched = state.with_repo_read(|b| b.get_reference(r.id)).unwrap(); assert_eq!( fetched.fields.get("title").map(String::as_str), Some("A Test Title") ); state.with_repo(|b| b.remove_field(r.id, "title")).unwrap(); let fetched2 = state.with_repo_read(|b| b.get_reference(r.id)).unwrap(); assert!(!fetched2.fields.contains_key("title")); } #[test] fn search_references_filters_by_query() { let (state, _tmp) = open_state(); state .with_repo(|b| b.create_reference("turing1950", EntryType::Article)) .unwrap(); state .with_repo(|b| b.create_reference("knuth1984", EntryType::Book)) .unwrap(); let results = state .with_repo_read(|b| b.search_references("turing")) .unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].cite_key, "turing1950"); } // ── Library ─────────────────────────────────────────────────────────────────── #[test] fn create_nested_libraries_and_query_hierarchy() { let (state, _tmp) = open_state(); let root = state.with_repo(|b| b.create_library("Root", None)).unwrap(); let child = state .with_repo(|b| b.create_library("Child", Some(root.id))) .unwrap(); let roots = state.with_repo_read(|b| b.list_root_libraries()).unwrap(); assert_eq!(roots.len(), 1); assert_eq!(roots[0].id, root.id); let children = state .with_repo_read(|b| b.list_child_libraries(root.id)) .unwrap(); assert_eq!(children.len(), 1); assert_eq!(children[0].id, child.id); let ancestors = state .with_repo_read(|b| b.get_library_ancestors(child.id)) .unwrap(); assert_eq!(ancestors.len(), 1); assert_eq!(ancestors[0].id, root.id); } #[test] fn add_reference_to_library_and_query() { let (state, _tmp) = open_state(); let r = state .with_repo(|b| b.create_reference("member2024", EntryType::Article)) .unwrap(); let lib = state.with_repo(|b| b.create_library("Lib", None)).unwrap(); state.with_repo(|b| b.add_to_library(lib.id, r.id)).unwrap(); let members = state .with_repo_read(|b| b.list_library_references(lib.id)) .unwrap(); assert_eq!(members.len(), 1); assert_eq!(members[0].id, r.id); } #[test] fn force_delete_library_removes_subtree() { let (state, _tmp) = open_state(); let root = state.with_repo(|b| b.create_library("Root", None)).unwrap(); state .with_repo(|b| b.create_library("Child", Some(root.id))) .unwrap(); state .with_repo(|b| b.force_delete_library(root.id)) .unwrap(); let all = state.with_repo_read(|b| b.list_root_libraries()).unwrap(); assert!(all.is_empty()); } // ── BibTeX export ───────────────────────────────────────────────────────────── #[test] fn export_library_bibtex_contains_entries() { let (state, _tmp) = open_state(); let mut r = state .with_repo(|b| b.create_reference("turing1950", EntryType::Article)) .unwrap(); r.authors.push(Person::new("Turing")); r.fields.insert( "title".into(), "Computing Machinery and Intelligence".into(), ); r.fields.insert("journal".into(), "Mind".into()); r.fields.insert("year".into(), "1950".into()); let r = state.with_repo(|b| b.update_reference(r)).unwrap(); let lib = state.with_repo(|b| b.create_library("CS", None)).unwrap(); state.with_repo(|b| b.add_to_library(lib.id, r.id)).unwrap(); let (bibtex, errors) = state .with_repo_read(|b| b.export_library_bibtex(lib.id)) .unwrap(); assert!(errors.is_empty()); assert!(bibtex.contains("@article{turing1950,")); assert!(bibtex.contains("Computing Machinery and Intelligence")); } // ── Snapshot ────────────────────────────────────────────────────────────────── #[test] fn snapshot_and_discard_changes() { let (state, _tmp) = open_state(); state .with_repo(|b| b.create_reference("snap2024", EntryType::Misc)) .unwrap(); let snap = state.with_repo(|b| b.create_snapshot("baseline")).unwrap(); assert!(!snap.id.is_empty()); let snapshots = state.with_repo_read(|b| b.list_snapshots()).unwrap(); assert!(snapshots.iter().any(|s| s.message == "baseline")); // Delete the reference (uncommitted change). let r_id = state .with_repo_read(|b| b.list_references()) .unwrap() .into_iter() .next() .unwrap() .id; state.with_repo(|b| b.delete_reference(r_id)).unwrap(); assert!(state .with_repo_read(|b| b.has_uncommitted_changes()) .unwrap()); // Discard → reference comes back. state.with_repo(|b| b.discard_changes()).unwrap(); let refs = state.with_repo_read(|b| b.list_references()).unwrap(); assert_eq!(refs.len(), 1); }