use tokmd_git::{GitCommit, GitRangeMode, classify_intent};
use tokmd_types::CommitIntentKind;
#[test]
fn git_commit_with_all_fields() {
let c = GitCommit {
timestamp: 1_700_000_000,
author: "dev@example.com".to_string(),
hash: Some("abc123def456".to_string()),
subject: "feat: add parser".to_string(),
files: vec!["src/parser.rs".to_string()],
};
assert_eq!(c.timestamp, 1_700_000_000);
assert_eq!(c.author, "dev@example.com");
assert_eq!(c.hash.as_deref(), Some("abc123def456"));
assert_eq!(c.files.len(), 1);
}
#[test]
fn git_commit_with_no_hash() {
let c = GitCommit {
timestamp: 0,
author: String::new(),
hash: None,
subject: String::new(),
files: vec![],
};
assert!(c.hash.is_none());
assert!(c.files.is_empty());
}
#[test]
fn range_mode_two_dot_format_with_tags() {
assert_eq!(
GitRangeMode::TwoDot.format("v1.0.0", "v2.0.0"),
"v1.0.0..v2.0.0"
);
}
#[test]
fn range_mode_three_dot_format_with_branches() {
assert_eq!(
GitRangeMode::ThreeDot.format("origin/main", "feature/abc"),
"origin/main...feature/abc"
);
}
#[test]
fn range_mode_default_is_two_dot() {
assert_eq!(GitRangeMode::default(), GitRangeMode::TwoDot);
}
#[test]
fn range_mode_equality() {
assert_eq!(GitRangeMode::TwoDot, GitRangeMode::TwoDot);
assert_eq!(GitRangeMode::ThreeDot, GitRangeMode::ThreeDot);
assert_ne!(GitRangeMode::TwoDot, GitRangeMode::ThreeDot);
}
#[test]
fn range_mode_format_with_empty_strings() {
assert_eq!(GitRangeMode::TwoDot.format("", ""), "..");
assert_eq!(GitRangeMode::ThreeDot.format("", ""), "...");
}
#[test]
fn classify_intent_conventional_feat() {
assert_eq!(
classify_intent("feat: add new parser"),
CommitIntentKind::Feat
);
}
#[test]
fn classify_intent_conventional_feat_with_scope() {
assert_eq!(
classify_intent("feat(cli): add verbose flag"),
CommitIntentKind::Feat
);
}
#[test]
fn classify_intent_conventional_fix() {
assert_eq!(
classify_intent("fix: correct null pointer"),
CommitIntentKind::Fix
);
}
#[test]
fn classify_intent_conventional_breaking_change() {
assert_eq!(
classify_intent("feat!: remove deprecated API"),
CommitIntentKind::Feat
);
}
#[test]
fn classify_intent_conventional_refactor() {
assert_eq!(
classify_intent("refactor: extract helper"),
CommitIntentKind::Refactor
);
}
#[test]
fn classify_intent_conventional_docs() {
assert_eq!(
classify_intent("docs: update README"),
CommitIntentKind::Docs
);
}
#[test]
fn classify_intent_conventional_test() {
assert_eq!(
classify_intent("test: add unit tests"),
CommitIntentKind::Test
);
}
#[test]
fn classify_intent_conventional_chore() {
assert_eq!(
classify_intent("chore: bump dependencies"),
CommitIntentKind::Chore
);
}
#[test]
fn classify_intent_conventional_ci() {
assert_eq!(
classify_intent("ci: add GitHub Actions workflow"),
CommitIntentKind::Ci
);
}
#[test]
fn classify_intent_conventional_build() {
assert_eq!(
classify_intent("build: update Cargo.toml"),
CommitIntentKind::Build
);
}
#[test]
fn classify_intent_conventional_perf() {
assert_eq!(
classify_intent("perf: optimize hot path"),
CommitIntentKind::Perf
);
}
#[test]
fn classify_intent_conventional_style() {
assert_eq!(
classify_intent("style: apply rustfmt"),
CommitIntentKind::Style
);
}
#[test]
fn classify_intent_revert_prefix() {
assert_eq!(
classify_intent("Revert \"feat: add parser\""),
CommitIntentKind::Revert
);
}
#[test]
fn classify_intent_revert_conventional() {
assert_eq!(
classify_intent("revert: undo breaking change"),
CommitIntentKind::Revert
);
}
#[test]
fn classify_intent_keyword_fix() {
assert_eq!(
classify_intent("Fix crash on empty input"),
CommitIntentKind::Fix
);
}
#[test]
fn classify_intent_keyword_add() {
assert_eq!(
classify_intent("Add support for YAML"),
CommitIntentKind::Feat
);
}
#[test]
fn classify_intent_keyword_implement() {
assert_eq!(
classify_intent("Implement caching layer"),
CommitIntentKind::Feat
);
}
#[test]
fn classify_intent_keyword_refactor() {
assert_eq!(
classify_intent("Refactor config loading"),
CommitIntentKind::Refactor
);
}
#[test]
fn classify_intent_keyword_doc() {
assert_eq!(
classify_intent("Update doc comments"),
CommitIntentKind::Docs
);
}
#[test]
fn classify_intent_keyword_readme() {
assert_eq!(
classify_intent("Update readme with examples"),
CommitIntentKind::Docs
);
}
#[test]
fn classify_intent_keyword_test() {
assert_eq!(
classify_intent("Extend test coverage"),
CommitIntentKind::Test
);
}
#[test]
fn classify_intent_keyword_optimize() {
assert_eq!(
classify_intent("Optimize memory usage"),
CommitIntentKind::Perf
);
}
#[test]
fn classify_intent_keyword_lint() {
assert_eq!(
classify_intent("Apply lint suggestions"),
CommitIntentKind::Style
);
}
#[test]
fn classify_intent_keyword_pipeline() {
assert_eq!(
classify_intent("Update pipeline config"),
CommitIntentKind::Ci
);
}
#[test]
fn classify_intent_keyword_deps() {
assert_eq!(
classify_intent("Bump deps to latest"),
CommitIntentKind::Build
);
}
#[test]
fn classify_intent_keyword_cleanup() {
assert_eq!(
classify_intent("Cleanup unused imports"),
CommitIntentKind::Chore
);
}
#[test]
fn classify_intent_empty_string() {
assert_eq!(classify_intent(""), CommitIntentKind::Other);
}
#[test]
fn classify_intent_whitespace_only() {
assert_eq!(classify_intent(" "), CommitIntentKind::Other);
}
#[test]
fn classify_intent_unknown_subject() {
assert_eq!(classify_intent("Initial commit"), CommitIntentKind::Other);
}
#[test]
fn classify_intent_case_insensitive_conventional() {
assert_eq!(
classify_intent("FEAT: loud feature"),
CommitIntentKind::Feat
);
assert_eq!(classify_intent("Fix: quiet fix"), CommitIntentKind::Fix);
}
#[test]
fn classify_intent_word_boundary_prevents_false_match() {
assert_ne!(classify_intent("prefix some change"), CommitIntentKind::Fix);
assert_ne!(
classify_intent("detest the linter"),
CommitIntentKind::Test,
"detest embeds 'test' but not at word boundary"
);
}
fn git_cmd() -> std::process::Command {
let mut cmd = std::process::Command::new("git");
cmd.env_remove("GIT_DIR").env_remove("GIT_WORK_TREE");
cmd
}
fn test_git(dir: &std::path::Path) -> std::process::Command {
let mut cmd = git_cmd();
cmd.arg("-C").arg(dir);
cmd
}
fn init_repo(dir: &std::path::Path) {
test_git(dir).arg("init").output().unwrap();
test_git(dir)
.args(["config", "user.email", "test@test.com"])
.output()
.unwrap();
test_git(dir)
.args(["config", "user.name", "Test User"])
.output()
.unwrap();
}
fn add_commit(dir: &std::path::Path, file: &str, content: &str, msg: &str) {
let path = dir.join(file);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&path, content).unwrap();
test_git(dir).args(["add", "."]).output().unwrap();
test_git(dir).args(["commit", "-m", msg]).output().unwrap();
}
#[test]
fn collect_history_empty_repo() {
if !tokmd_git::git_available() {
return;
}
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
let result = tokmd_git::collect_history(dir.path(), None, None);
if let Ok(commits) = result {
assert!(commits.is_empty());
}
}
#[test]
fn collect_history_single_commit() {
if !tokmd_git::git_available() {
return;
}
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
add_commit(dir.path(), "main.rs", "fn main() {}", "feat: init");
let commits = tokmd_git::collect_history(dir.path(), None, None).unwrap();
assert_eq!(commits.len(), 1);
assert_eq!(commits[0].author, "test@test.com");
assert_eq!(commits[0].subject, "feat: init");
assert!(commits[0].files.contains(&"main.rs".to_string()));
assert!(commits[0].hash.is_some());
assert!(commits[0].timestamp > 0);
}
#[test]
fn collect_history_multiple_commits_order() {
if !tokmd_git::git_available() {
return;
}
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
add_commit(dir.path(), "a.rs", "1", "first");
add_commit(dir.path(), "b.rs", "2", "second");
add_commit(dir.path(), "c.rs", "3", "third");
let commits = tokmd_git::collect_history(dir.path(), None, None).unwrap();
assert_eq!(commits.len(), 3);
assert_eq!(commits[0].subject, "third");
assert_eq!(commits[1].subject, "second");
assert_eq!(commits[2].subject, "first");
}
#[test]
fn collect_history_max_commits_limit() {
if !tokmd_git::git_available() {
return;
}
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
for i in 0..5 {
add_commit(
dir.path(),
&format!("f{i}.rs"),
&format!("{i}"),
&format!("commit {i}"),
);
}
let result = tokmd_git::collect_history(dir.path(), Some(2), None);
if let Ok(commits) = result {
assert!(commits.len() <= 2, "max_commits should limit to 2");
}
}
#[test]
fn collect_history_max_commit_files_limit() {
if !tokmd_git::git_available() {
return;
}
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
for i in 0..10 {
let path = dir.path().join(format!("file{i}.rs"));
std::fs::write(&path, format!("content {i}")).unwrap();
}
test_git(dir.path()).args(["add", "."]).output().unwrap();
test_git(dir.path())
.args(["commit", "-m", "many files"])
.output()
.unwrap();
let commits = tokmd_git::collect_history(dir.path(), None, Some(3)).unwrap();
assert_eq!(commits.len(), 1);
assert!(
commits[0].files.len() <= 3,
"max_commit_files should limit files per commit"
);
}
#[test]
fn collect_history_subdirectory_files() {
if !tokmd_git::git_available() {
return;
}
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
add_commit(
dir.path(),
"src/lib.rs",
"pub fn hello() {}",
"feat: add lib",
);
let commits = tokmd_git::collect_history(dir.path(), None, None).unwrap();
assert_eq!(commits.len(), 1);
assert!(commits[0].files.contains(&"src/lib.rs".to_string()));
}
#[test]
fn rev_exists_on_nonexistent_dir() {
let dir = std::path::PathBuf::from("/nonexistent/path/xyz123");
assert!(!tokmd_git::rev_exists(&dir, "HEAD"));
}
#[test]
fn rev_exists_non_repo_directory() {
let dir = tempfile::tempdir().unwrap();
assert!(!tokmd_git::rev_exists(dir.path(), "HEAD"));
}
#[test]
fn rev_exists_head_after_commit() {
if !tokmd_git::git_available() {
return;
}
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
add_commit(dir.path(), "f.txt", "x", "init");
assert!(tokmd_git::rev_exists(dir.path(), "HEAD"));
}
#[test]
fn rev_exists_bogus_ref() {
if !tokmd_git::git_available() {
return;
}
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
add_commit(dir.path(), "f.txt", "x", "init");
assert!(!tokmd_git::rev_exists(dir.path(), "nonexistent-branch-xyz"));
}
#[test]
fn repo_root_returns_some_for_git_repo() {
if !tokmd_git::git_available() {
return;
}
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
add_commit(dir.path(), "f.txt", "x", "init");
let root = tokmd_git::repo_root(dir.path());
assert!(root.is_some(), "repo_root should find git repo");
}
#[test]
fn repo_root_returns_none_for_non_repo() {
let dir = tempfile::tempdir().unwrap();
assert!(tokmd_git::repo_root(dir.path()).is_none());
}
#[test]
fn git_available_returns_bool() {
let _available: bool = tokmd_git::git_available();
}
#[test]
fn resolve_base_ref_returns_none_for_explicit_nonexistent() {
if !tokmd_git::git_available() {
return;
}
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
add_commit(dir.path(), "f.txt", "x", "init");
assert_eq!(tokmd_git::resolve_base_ref(dir.path(), "v99.99.99"), None);
}