//! End-to-end integration tests driving the `levcs` binary like a user. use std::process::Command; fn levcs_bin() -> String { env!("CARGO_BIN_EXE_levcs").to_string() } fn run(args: &[&str], cwd: &std::path::Path, xdg: &std::path::Path) -> (i32, String, String) { let out = Command::new(levcs_bin()) .args(args) .current_dir(cwd) .env("XDG_CONFIG_HOME", xdg) .output() .expect("run levcs"); ( out.status.code().unwrap_or(-1), String::from_utf8_lossy(&out.stdout).to_string(), String::from_utf8_lossy(&out.stderr).to_string(), ) } fn tempdir(prefix: &str) -> std::path::PathBuf { let mut p = std::env::temp_dir(); let n = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_nanos()) .unwrap_or(0); p.push(format!("{prefix}-{n}-{}", std::process::id())); std::fs::create_dir_all(&p).unwrap(); p } #[test] fn end_to_end_init_track_commit_log_verify() { let work = tempdir("levcs-it"); let xdg = work.join("cfg"); std::fs::create_dir_all(&xdg).unwrap(); // init let (code, _o, e) = run(&["init", "--key", "alice"], &work, &xdg); assert_eq!(code, 0, "init failed: {e}"); // write file, track, commit std::fs::write(work.join("a.txt"), b"hello\n").unwrap(); let (code, _, e) = run(&["track", "--all"], &work, &xdg); assert_eq!(code, 0, "track failed: {e}"); let (code, _, e) = run(&["commit", "-m", "first"], &work, &xdg); assert_eq!(code, 0, "commit failed: {e}"); // status should be clean let (code, o, _) = run(&["status"], &work, &xdg); assert_eq!(code, 0); assert!(o.contains("working tree clean")); // verify let (code, _, e) = run(&["verify"], &work, &xdg); assert_eq!(code, 0, "verify failed: {e}"); // log should show one commit let (code, o, _) = run(&["log"], &work, &xdg); assert_eq!(code, 0); assert!(o.contains("first")); let _ = std::fs::remove_dir_all(work); } #[test] fn authority_chain_round_trip() { let work = tempdir("levcs-auth"); let xdg = work.join("cfg"); std::fs::create_dir_all(&xdg).unwrap(); let (code, _, e) = run(&["init", "--key", "alice"], &work, &xdg); assert_eq!(code, 0, "init: {e}"); std::fs::write(work.join("README"), b"hi\n").unwrap(); run(&["track", "--all"], &work, &xdg); run(&["commit", "-m", "init"], &work, &xdg); // Generate bob and add as contributor let (_, _, _) = run(&["key", "generate", "bob"], &work, &xdg); let (_, bob_pub, _) = run(&["key", "show", "bob"], &work, &xdg); let bob_pub = bob_pub.trim().to_string(); let (code, _, e) = run( &[ "authority", "add", &bob_pub, "--role", "contributor", "--handle", "bob", ], &work, &xdg, ); assert_eq!(code, 0, "authority add: {e}"); // Bob commits std::fs::write(work.join("BOB"), b"bob's note\n").unwrap(); run(&["track", "--all"], &work, &xdg); let (code, _, e) = run(&["commit", "-m", "bob's edit", "--key", "bob"], &work, &xdg); assert_eq!(code, 0, "bob's commit: {e}"); // Verify the entire chain let (code, _, e) = run(&["verify"], &work, &xdg); assert_eq!(code, 0, "verify: {e}"); let _ = std::fs::remove_dir_all(work); } #[test] fn branch_create_and_switch() { let work = tempdir("levcs-branch"); let xdg = work.join("cfg"); std::fs::create_dir_all(&xdg).unwrap(); run(&["init", "--key", "alice"], &work, &xdg); std::fs::write(work.join("a.txt"), b"hi\n").unwrap(); run(&["track", "--all"], &work, &xdg); run(&["commit", "-m", "first"], &work, &xdg); let (code, _, _) = run(&["branch", "--create", "dev"], &work, &xdg); assert_eq!(code, 0); let (_, o, _) = run(&["branch", "--list"], &work, &xdg); assert!(o.contains("dev")); assert!(o.contains("main")); let _ = std::fs::remove_dir_all(work); } /// `gc` keeps unreachable objects newer than the grace period and /// removes them once the period expires (§4.2.2). Drop a synthetic /// hex-named file into the object store to give gc something /// unreachable to reason about. #[test] fn gc_grace_period_keeps_young_objects_and_deletes_old_ones() { let work = tempdir("levcs-gc"); let xdg = work.join("cfg"); std::fs::create_dir_all(&xdg).unwrap(); run(&["init", "--key", "alice"], &work, &xdg); std::fs::write(work.join("a.txt"), b"hi\n").unwrap(); run(&["track", "--all"], &work, &xdg); run(&["commit", "-m", "first"], &work, &xdg); // Drop a hex-named "object" file into the sharded store. The // contents are arbitrary; gc's only reachability rule is "is the // hash visible from any ref?", so this name is unreachable by // construction. let stray_dir = work.join(".levcs/objects/ff"); std::fs::create_dir_all(&stray_dir).unwrap(); // The shard prefix is the first 2 hex chars; the on-disk filename // is the *remaining* 62. iter_ids reconstructs the full 64-char // hash from prefix + filename. let stray = stray_dir.join("ff".repeat(31)); std::fs::write(&stray, b"unreachable garbage").unwrap(); assert!(stray.is_file()); // Default grace (14 days) — the just-written stray file is way // younger than that, so it must be kept. let (code, _, e) = run(&["gc"], &work, &xdg); assert_eq!(code, 0, "gc default: {e}"); assert!( stray.is_file(), "young unreachable object must be kept under default grace" ); assert!(e.contains("kept"), "gc must report kept count: {e}"); // Force grace=0 and the stray file must go. let (code, _, e) = run(&["gc", "--grace-days=0"], &work, &xdg); assert_eq!(code, 0, "gc grace=0: {e}"); assert!( !stray.is_file(), "with grace=0 the unreachable object must be deleted" ); assert!(e.contains("removed"), "gc must report deletion count: {e}"); let _ = std::fs::remove_dir_all(&work); }