//! Integration tests for kei-ledger. //! //! Constructor Pattern: each test = one scenario, one assertion target. //! Uses tempfile for per-test isolated sqlite file. Loads source modules //! via `#[path]` so we don't need to expose a library crate surface. #[path = "../src/schema.rs"] mod schema; #[path = "../src/ledger.rs"] mod ledger; use rusqlite::Connection; use std::fs; use std::path::{Path, PathBuf}; use tempfile::TempDir; fn open_tmp() -> (TempDir, Connection) { let dir = tempfile::tempdir().unwrap(); let db = dir.path().join("ledger.sqlite"); let conn = ledger::open(&db).unwrap(); (dir, conn) } fn write_artefacts(root: &Path, agent_id: &str, which: &[&str]) -> PathBuf { let base = root.join(".claude/agents").join(agent_id); fs::create_dir_all(&base).unwrap(); for f in which { fs::write(base.join(f), b"x").unwrap(); } base } #[test] fn fork_then_done_marks_terminal() { let (_d, conn) = open_tmp(); ledger::fork(&conn, "a1", "agent/a1", None, "deadbeef", None).unwrap(); let running = ledger::list(&conn, Some("running")).unwrap(); assert_eq!(running.len(), 1); assert_eq!(running[0].id, "a1"); let updated = ledger::done(&conn, "a1", "shipped").unwrap(); assert_eq!(updated, 1); let done = ledger::list(&conn, Some("done")).unwrap(); assert_eq!(done.len(), 1); assert_eq!(done[0].summary.as_deref(), Some("shipped")); } #[test] fn fail_flow_sets_reason_and_finished_ts() { let (_d, conn) = open_tmp(); ledger::fork(&conn, "b1", "agent/b1", Some("main"), "cafebabe", None).unwrap(); let updated = ledger::fail(&conn, "b1", "cargo build failed").unwrap(); assert_eq!(updated, 1); let failed = ledger::list(&conn, Some("failed")).unwrap(); assert_eq!(failed.len(), 1); assert!(failed[0].finished_ts.is_some()); assert_eq!(failed[0].summary.as_deref(), Some("cargo build failed")); } #[test] fn tree_walks_parent_child_chain() { let (_d, conn) = open_tmp(); ledger::fork(&conn, "root", "agent/root", Some("main"), "aa", None).unwrap(); ledger::fork(&conn, "c1", "agent/c1", Some("agent/root"), "bb", None).unwrap(); ledger::fork(&conn, "c2", "agent/c2", Some("agent/root"), "cc", None).unwrap(); ledger::fork(&conn, "g1", "agent/g1", Some("agent/c1"), "dd", None).unwrap(); let t = ledger::tree(&conn, "root").unwrap(); let ids: Vec<_> = t.iter().map(|a| a.id.as_str()).collect(); assert!(ids.contains(&"root")); assert!(ids.contains(&"c1")); assert!(ids.contains(&"c2")); assert!(ids.contains(&"g1")); assert_eq!(ids[0], "root"); assert_eq!(ids.len(), 4); } #[test] fn list_filter_status_excludes_others() { let (_d, conn) = open_tmp(); ledger::fork(&conn, "r1", "br-r1", None, "s1", None).unwrap(); ledger::fork(&conn, "r2", "br-r2", None, "s2", None).unwrap(); ledger::done(&conn, "r1", "ok").unwrap(); let running = ledger::list(&conn, Some("running")).unwrap(); assert_eq!(running.len(), 1); assert_eq!(running[0].id, "r2"); let all = ledger::list(&conn, None).unwrap(); assert_eq!(all.len(), 2); } #[test] fn validate_detects_missing_artefacts() { let (d, _conn) = open_tmp(); write_artefacts(d.path(), "v1", &["spec.md", "plan.md"]); let missing = ledger::validate(d.path(), "v1"); assert_eq!(missing.len(), 4); assert!(missing.contains(&"progress.json".to_string())); assert!(missing.contains(&"review.md".to_string())); } #[test] fn validate_ok_when_all_six_present() { let (d, _conn) = open_tmp(); write_artefacts( d.path(), "v2", &[ "spec.md", "plan.md", "progress.json", "chatlog.md", "handoffs.md", "review.md", ], ); let missing = ledger::validate(d.path(), "v2"); assert!(missing.is_empty(), "got missing {missing:?}"); } #[test] fn duplicate_fork_id_rejected() { let (_d, conn) = open_tmp(); ledger::fork(&conn, "dup", "br1", None, "x", None).unwrap(); let err = ledger::fork(&conn, "dup", "br2", None, "y", None); assert!(err.is_err(), "duplicate id must fail"); } #[test] fn done_on_already_done_agent_is_noop() { let (_d, conn) = open_tmp(); ledger::fork(&conn, "n1", "br-n1", None, "h", None).unwrap(); assert_eq!(ledger::done(&conn, "n1", "first").unwrap(), 1); assert_eq!(ledger::done(&conn, "n1", "second").unwrap(), 0); let row = &ledger::list(&conn, None).unwrap()[0]; assert_eq!(row.summary.as_deref(), Some("first")); } #[test] fn merged_after_done_transitions_status() { let (_d, conn) = open_tmp(); ledger::fork(&conn, "m1", "br-m1", None, "h", None).unwrap(); ledger::done(&conn, "m1", "ready").unwrap(); assert_eq!(ledger::merged(&conn, "m1").unwrap(), 1); let merged = ledger::list(&conn, Some("merged")).unwrap(); assert_eq!(merged.len(), 1); assert_eq!(merged[0].summary.as_deref(), Some("ready")); }