use std::path::Path;
use assert_cmd::Command;
use chrono::Utc;
use predicates::prelude::*;
fn seshat() -> Command {
let home = tempfile::tempdir().expect("create isolated HOME tempdir");
let home_path = home.keep();
let mut cmd = Command::cargo_bin("seshat").expect("binary exists");
cmd.env("HOME", &home_path);
cmd.env_remove("XDG_DATA_HOME");
cmd
}
struct ProjectDbGuard {
db_file: Option<std::path::PathBuf>,
}
impl ProjectDbGuard {
fn new(scanned_path: &Path) -> Self {
let dir_name = scanned_path
.file_name()
.expect("scanned path has a file_name component")
.to_string_lossy()
.to_string();
let db_file = dirs::data_dir().map(|d| {
d.join("seshat")
.join("repos")
.join(format!("{dir_name}.db"))
});
Self { db_file }
}
}
impl Drop for ProjectDbGuard {
fn drop(&mut self) {
if let Some(ref db_file) = self.db_file {
for ext in ["", "-wal", "-shm"] {
let path = if ext.is_empty() {
db_file.clone()
} else {
let name = format!("{}{ext}", db_file.file_name().unwrap().to_string_lossy());
db_file.with_file_name(name)
};
let _ = std::fs::remove_file(&path);
}
}
}
}
#[test]
fn scan_nonexistent_path_exits_with_error() {
seshat()
.args(["scan", "/tmp/seshat-test-nonexistent-path-12345"])
.assert()
.failure()
.stderr(predicates::str::contains("error:"))
.stderr(predicates::str::contains("does not exist"));
}
#[test]
fn scan_file_instead_of_directory_exits_with_error() {
let tmp = tempfile::NamedTempFile::new().expect("create temp file");
let path = tmp.path().to_str().expect("valid path");
seshat()
.args(["scan", path])
.assert()
.failure()
.stderr(predicates::str::contains("error:"))
.stderr(predicates::str::contains("not a directory"));
}
#[test]
fn scan_empty_directory_succeeds_with_warning() {
let tmp = tempfile::tempdir().expect("create temp dir");
let _guard = ProjectDbGuard::new(tmp.path());
seshat()
.args(["scan", tmp.path().to_str().expect("valid path")])
.assert()
.success()
.stderr(predicates::str::contains("Scanned 0 files"))
.stderr(predicates::str::contains("no files discovered"));
}
#[test]
fn scan_fixture_project_succeeds() {
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("parent")
.parent()
.expect("workspace root")
.join("tests/fixtures/rust_project");
if !fixture.exists() {
return;
}
seshat()
.env("NO_COLOR", "1")
.args(["scan", fixture.to_str().expect("valid path")])
.assert()
.success()
.stderr(predicates::str::contains("Scanned"))
.stderr(predicates::str::contains("Completed in"));
}
#[test]
fn scan_directory_with_no_parseable_files_succeeds() {
let tmp = tempfile::tempdir().expect("create temp dir");
let _guard = ProjectDbGuard::new(tmp.path());
std::fs::write(tmp.path().join("readme.txt"), "hello").expect("write file");
std::fs::write(tmp.path().join("data.csv"), "a,b,c").expect("write file");
seshat()
.args(["scan", tmp.path().to_str().expect("valid path")])
.assert()
.success()
.stderr(predicates::str::contains("Scanned 0 files"));
}
#[test]
fn scan_quiet_mode_minimal_output() {
let tmp = tempfile::tempdir().expect("create temp dir");
let _guard = ProjectDbGuard::new(tmp.path());
seshat()
.args(["scan", tmp.path().to_str().expect("valid path"), "--quiet"])
.assert()
.success()
.stderr(predicates::str::contains("Scanned"))
.stderr(predicates::str::contains("Completed in"))
.stderr(predicates::str::contains("seshat v").not());
}
#[test]
fn scan_verbose_mode_shows_timing() {
let tmp = tempfile::tempdir().expect("create temp dir");
let _guard = ProjectDbGuard::new(tmp.path());
seshat()
.args([
"scan",
tmp.path().to_str().expect("valid path"),
"--verbose",
])
.assert()
.success()
.stderr(predicates::str::contains("Timing"))
.stderr(predicates::str::contains("Total:"));
}
#[test]
fn version_flag_prints_version() {
seshat()
.arg("--version")
.assert()
.success()
.stdout(predicates::str::contains("seshat"));
}
#[test]
fn no_subcommand_shows_help() {
seshat().assert().failure();
}
#[test]
fn serve_starts_and_shows_startup_info() {
let tmp_home = tempfile::tempdir().expect("create home temp dir");
let tmp_cwd = tempfile::tempdir().expect("create cwd temp dir");
std::process::Command::new("git")
.args(["init", "--quiet", "-b", "main"])
.current_dir(tmp_cwd.path())
.output()
.expect("git init");
#[cfg(target_os = "macos")]
let repos_dir = tmp_home
.path()
.join("Library")
.join("Application Support")
.join("seshat")
.join("repos");
#[cfg(not(target_os = "macos"))]
let repos_dir = tmp_home
.path()
.join(".local")
.join("share")
.join("seshat")
.join("repos");
std::fs::create_dir_all(&repos_dir).expect("create repos dir");
seshat_with_home(tmp_home.path())
.env("NO_COLOR", "1")
.current_dir(tmp_cwd.path())
.arg("serve")
.timeout(std::time::Duration::from_secs(25))
.assert()
.failure()
.stderr(predicates::str::contains(
"Waiting for MCP client connection",
));
}
#[test]
fn status_shows_output() {
seshat().arg("status").assert().success();
}
#[test]
fn review_in_git_repo_requires_scan_first() {
let tmp = tempfile::tempdir().expect("create temp dir");
std::process::Command::new("git")
.args(["init", "-b", "main"])
.current_dir(tmp.path())
.output()
.expect("git init");
seshat()
.current_dir(tmp.path())
.arg("review")
.assert()
.failure()
.stderr(predicates::str::contains("No database found"));
}
#[test]
fn review_without_git_repo_requires_scan_first() {
let tmp = tempfile::tempdir().expect("create temp dir");
seshat()
.current_dir(tmp.path())
.arg("review")
.assert()
.failure()
.stderr(predicates::str::contains("No database found"));
}
#[test]
fn init_auto_detects_clients() {
seshat().arg("init").assert().success();
}
#[test]
fn init_unknown_client_exits_error() {
seshat()
.arg("init")
.arg("vscode")
.assert()
.failure()
.stderr(predicates::str::contains("Unknown client"));
}
#[test]
fn init_dry_run_flag_accepted() {
seshat().arg("init").arg("--dry-run").assert().success();
}
fn version_cache_path_for_home(fake_home: &std::path::Path) -> std::path::PathBuf {
#[cfg(target_os = "macos")]
let data_dir = fake_home.join("Library").join("Application Support");
#[cfg(not(target_os = "macos"))]
let data_dir = fake_home.join(".local").join("share");
data_dir.join("seshat").join("version-check.json")
}
fn write_version_cache_for_home(fake_home: &std::path::Path, version: &str) {
let cache_file = version_cache_path_for_home(fake_home);
if let Some(parent) = cache_file.parent() {
std::fs::create_dir_all(parent).expect("create cache dir");
}
let now = Utc::now().to_rfc3339();
let json = format!(r#"{{"latest_version":"{version}","checked_at":"{now}"}}"#);
std::fs::write(&cache_file, json).expect("write cache file");
}
fn seshat_with_home(fake_home: &std::path::Path) -> Command {
let mut cmd = seshat();
cmd.env("HOME", fake_home);
cmd
}
#[test]
fn update_notice_printed_for_status_when_newer_version_cached() {
let tmp = tempfile::tempdir().expect("create temp dir");
write_version_cache_for_home(tmp.path(), "9999.0.0");
seshat_with_home(tmp.path())
.arg("status")
.assert()
.stderr(predicates::str::contains("Seshat v9999.0.0 is available"));
}
#[test]
fn update_notice_printed_for_scan_when_newer_version_cached() {
let tmp_home = tempfile::tempdir().expect("create home temp dir");
write_version_cache_for_home(tmp_home.path(), "9999.0.0");
let tmp_scan = tempfile::tempdir().expect("create scan temp dir");
let _guard = ProjectDbGuard::new(tmp_scan.path());
seshat_with_home(tmp_home.path())
.args(["scan", tmp_scan.path().to_str().expect("valid path")])
.assert()
.stderr(predicates::str::contains("Seshat v9999.0.0 is available"));
}
#[test]
fn update_notice_suppressed_for_seshat_update_check() {
let tmp = tempfile::tempdir().expect("create temp dir");
write_version_cache_for_home(tmp.path(), "9999.0.0");
seshat_with_home(tmp.path())
.args(["update", "--check"])
.assert()
.stderr(predicates::str::contains("Run seshat update to upgrade.").not());
}
#[test]
fn update_notice_suppressed_for_seshat_update() {
let tmp = tempfile::tempdir().expect("create temp dir");
write_version_cache_for_home(tmp.path(), "9999.0.0");
seshat_with_home(tmp.path())
.arg("update")
.assert()
.stderr(predicates::str::contains("Run seshat update to upgrade.").not());
}
#[test]
fn update_notice_not_printed_when_up_to_date() {
let tmp = tempfile::tempdir().expect("create temp dir");
write_version_cache_for_home(tmp.path(), "0.0.1");
seshat_with_home(tmp.path())
.arg("status")
.assert()
.stderr(predicates::str::contains("is available").not());
}
#[test]
fn update_notice_network_failure_silent_skip() {
let tmp = tempfile::tempdir().expect("create temp dir");
seshat_with_home(tmp.path())
.arg("status")
.assert()
.stderr(predicates::str::contains("Could not check for updates").not());
}