use std::path::Path;
use std::time::{Duration, Instant};
use super::*;
#[test]
fn parsed_front_matter_default() {
let fm = ParsedFrontMatter::default();
assert!(fm.title.is_none());
assert!(fm.tags.is_none());
assert!(fm.raw_yaml.is_none());
}
#[test]
fn parsed_front_matter_debug() {
let fm = ParsedFrontMatter {
title: Some("Test".to_string()),
tags: Some("rust,testing".to_string()),
raw_yaml: Some("title: Test".to_string()),
};
let debug = format!("{fm:?}");
assert!(debug.contains("Test"));
assert!(debug.contains("rust,testing"));
}
#[test]
fn ingest_summary_default() {
let summary = IngestSummary::default();
assert_eq!(summary.ingested, 0);
assert_eq!(summary.skipped, 0);
assert!(summary.errors.is_empty());
}
#[test]
fn ingest_summary_debug() {
let summary = IngestSummary {
ingested: 5,
skipped: 2,
errors: vec!["file.md: error".to_string()],
};
let debug = format!("{summary:?}");
assert!(debug.contains("5"));
assert!(debug.contains("2"));
assert!(debug.contains("file.md"));
}
#[test]
fn parse_front_matter_with_yaml() {
let content = "---\ntitle: Hello\ntags:\n - rust\n - test\n---\nBody text here";
let (fm, body) = parse_front_matter(content);
assert_eq!(fm.title.as_deref(), Some("Hello"));
assert_eq!(fm.tags.as_deref(), Some("rust,test"));
assert!(fm.raw_yaml.is_some());
assert_eq!(body.trim(), "Body text here");
}
#[test]
fn parse_front_matter_no_yaml() {
let content = "Just plain text without front matter.";
let (fm, body) = parse_front_matter(content);
assert!(fm.title.is_none());
assert!(fm.tags.is_none());
assert!(fm.raw_yaml.is_none());
assert_eq!(body, content);
}
#[test]
fn parse_front_matter_empty_yaml() {
let content = "---\n---\nBody";
let (fm, body) = parse_front_matter(content);
assert!(fm.title.is_none());
assert!(fm.tags.is_none());
assert!(body.contains("Body"));
}
#[test]
fn parse_front_matter_tags_as_string() {
let content = "---\ntags: \"single-tag\"\n---\nBody";
let (fm, _) = parse_front_matter(content);
assert_eq!(fm.tags.as_deref(), Some("single-tag"));
}
#[test]
fn parse_front_matter_invalid_yaml() {
let content = "---\n: invalid yaml [[\n---\nBody";
let (fm, body) = parse_front_matter(content);
assert!(fm.raw_yaml.is_some());
assert!(fm.title.is_none());
assert_eq!(body.trim(), "Body");
}
#[test]
fn matches_patterns_md_extension() {
assert!(matches_patterns(Path::new("doc.md"), &["*.md".to_string()]));
}
#[test]
fn matches_patterns_txt_extension() {
assert!(matches_patterns(
Path::new("notes.txt"),
&["*.txt".to_string()]
));
}
#[test]
fn matches_patterns_no_match() {
assert!(!matches_patterns(
Path::new("image.png"),
&["*.md".to_string(), "*.txt".to_string()]
));
}
#[test]
fn matches_patterns_nested_path() {
assert!(matches_patterns(
Path::new("sub/dir/note.md"),
&["*.md".to_string()]
));
}
#[test]
fn matches_patterns_empty_patterns() {
assert!(!matches_patterns(Path::new("file.md"), &[]));
}
#[test]
fn matches_patterns_multiple_patterns() {
assert!(matches_patterns(
Path::new("doc.md"),
&["*.txt".to_string(), "*.md".to_string()]
));
}
#[test]
fn matches_patterns_invalid_pattern_ignored() {
assert!(!matches_patterns(
Path::new("file.md"),
&["[invalid".to_string()]
));
}
#[test]
fn relative_path_string_simple() {
let result = relative_path_string(Path::new("file.md"));
assert_eq!(result, "file.md");
}
#[test]
fn relative_path_string_nested() {
let result = relative_path_string(Path::new("sub/dir/file.md"));
assert_eq!(result, "sub/dir/file.md");
}
#[test]
fn cooldown_set_new_is_empty() {
let cd = CooldownSet::new(Duration::from_secs(5));
assert!(!cd.is_cooling(Path::new("/test/path")));
}
#[test]
fn cooldown_set_mark_and_check() {
let mut cd = CooldownSet::new(Duration::from_secs(60));
let path = std::path::PathBuf::from("/test/file.md");
cd.mark(path.clone());
assert!(cd.is_cooling(&path));
}
#[test]
fn cooldown_set_cleanup_removes_expired() {
let mut cd = CooldownSet::new(Duration::from_millis(1));
let path = std::path::PathBuf::from("/test/old.md");
cd.entries
.insert(path.clone(), Instant::now() - Duration::from_secs(10));
cd.cleanup();
assert!(!cd.is_cooling(&path));
assert!(cd.entries.is_empty());
}
#[test]
fn cooldown_set_cleanup_keeps_recent() {
let mut cd = CooldownSet::new(Duration::from_secs(60));
let path = std::path::PathBuf::from("/test/recent.md");
cd.mark(path.clone());
cd.cleanup();
assert!(cd.is_cooling(&path));
}
#[test]
fn watchtower_error_io_display() {
let err = WatchtowerError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"missing file",
));
let msg = err.to_string();
assert!(msg.contains("IO error"));
assert!(msg.contains("missing file"));
}
#[test]
fn watchtower_error_config_display() {
let err = WatchtowerError::Config("bad config".to_string());
assert_eq!(err.to_string(), "config error: bad config");
}
#[test]
fn watchtower_error_config_display_2() {
let err = WatchtowerError::Config("missing source".to_string());
let msg = err.to_string();
assert!(msg.contains("config error"));
assert!(msg.contains("missing source"));
}
#[test]
fn watchtower_error_debug() {
let err = WatchtowerError::Config("test".to_string());
let debug = format!("{err:?}");
assert!(debug.contains("Config"));
}
#[test]
fn watchtower_loop_defaults() {
assert_eq!(std::time::Duration::from_secs(2).as_secs(), 2);
assert_eq!(std::time::Duration::from_secs(300).as_secs(), 300);
assert_eq!(std::time::Duration::from_secs(5).as_secs(), 5);
}
#[test]
fn walk_directory_finds_matching_files() {
let dir = tempfile::tempdir().expect("temp dir");
let base = dir.path();
std::fs::write(base.join("note.md"), "# Note").expect("write md");
std::fs::write(base.join("readme.txt"), "hello").expect("write txt");
std::fs::write(base.join("image.png"), "fake").expect("write png");
let mut out = Vec::new();
WatchtowerLoop::walk_directory(
base,
base,
&["*.md".to_string(), "*.txt".to_string()],
&mut out,
)
.expect("walk");
assert_eq!(out.len(), 2);
assert!(out.contains(&"note.md".to_string()));
assert!(out.contains(&"readme.txt".to_string()));
}
#[test]
fn walk_directory_recurses_into_subdirs() {
let dir = tempfile::tempdir().expect("temp dir");
let base = dir.path();
let sub = base.join("sub");
std::fs::create_dir(&sub).expect("mkdir");
std::fs::write(sub.join("deep.md"), "deep").expect("write");
let mut out = Vec::new();
WatchtowerLoop::walk_directory(base, base, &["*.md".to_string()], &mut out).expect("walk");
assert_eq!(out.len(), 1);
assert_eq!(out[0], "sub/deep.md");
}
#[test]
fn walk_directory_skips_hidden_dirs() {
let dir = tempfile::tempdir().expect("temp dir");
let base = dir.path();
let hidden = base.join(".hidden");
std::fs::create_dir(&hidden).expect("mkdir");
std::fs::write(hidden.join("secret.md"), "secret").expect("write");
std::fs::write(base.join("visible.md"), "visible").expect("write");
let mut out = Vec::new();
WatchtowerLoop::walk_directory(base, base, &["*.md".to_string()], &mut out).expect("walk");
assert_eq!(out.len(), 1);
assert_eq!(out[0], "visible.md");
}
#[test]
fn walk_directory_empty_dir() {
let dir = tempfile::tempdir().expect("temp dir");
let mut out = Vec::new();
WatchtowerLoop::walk_directory(dir.path(), dir.path(), &["*.md".to_string()], &mut out)
.expect("walk");
assert!(out.is_empty());
}
#[test]
fn walk_directory_no_matching_patterns() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::write(dir.path().join("data.csv"), "a,b").expect("write");
let mut out = Vec::new();
WatchtowerLoop::walk_directory(dir.path(), dir.path(), &["*.md".to_string()], &mut out)
.expect("walk");
assert!(out.is_empty());
}
#[test]
fn relative_path_string_empty() {
let result = relative_path_string(Path::new(""));
assert_eq!(result, "");
}
#[test]
fn relative_path_string_deeply_nested() {
let result = relative_path_string(Path::new("a/b/c/d/e/f.md"));
assert_eq!(result, "a/b/c/d/e/f.md");
}
#[test]
fn parse_front_matter_title_only() {
let content = "---\ntitle: My Title\n---\nBody text";
let (fm, body) = parse_front_matter(content);
assert_eq!(fm.title.as_deref(), Some("My Title"));
assert!(fm.tags.is_none());
assert_eq!(body.trim(), "Body text");
}
#[test]
fn parse_front_matter_empty_tags_list() {
let content = "---\ntags: []\n---\nBody";
let (fm, _) = parse_front_matter(content);
assert!(fm.tags.is_none());
}
#[test]
fn parse_front_matter_multiple_tags() {
let content = "---\ntags:\n - alpha\n - beta\n - gamma\n---\nContent";
let (fm, _) = parse_front_matter(content);
assert_eq!(fm.tags.as_deref(), Some("alpha,beta,gamma"));
}
#[test]
fn cooldown_set_different_paths_independent() {
let mut cd = CooldownSet::new(Duration::from_secs(60));
let path_a = std::path::PathBuf::from("/a.md");
let path_b = std::path::PathBuf::from("/b.md");
cd.mark(path_a.clone());
assert!(cd.is_cooling(&path_a));
assert!(!cd.is_cooling(&path_b));
}
#[test]
fn cooldown_set_re_mark_refreshes() {
let mut cd = CooldownSet::new(Duration::from_secs(60));
let path = std::path::PathBuf::from("/test.md");
cd.mark(path.clone());
cd.mark(path.clone());
assert!(cd.is_cooling(&path));
assert_eq!(cd.entries.len(), 1);
}
#[test]
fn matches_patterns_star_matches_all() {
assert!(matches_patterns(
Path::new("anything.xyz"),
&["*".to_string()]
));
}
#[test]
fn matches_patterns_specific_filename() {
assert!(matches_patterns(
Path::new("Makefile"),
&["Makefile".to_string()]
));
assert!(!matches_patterns(
Path::new("Dockerfile"),
&["Makefile".to_string()]
));
}
#[test]
fn watchtower_error_storage_display() {
let err = WatchtowerError::Config("missing source path".to_string());
let msg = err.to_string();
assert!(msg.contains("config error"));
assert!(msg.contains("missing source path"));
}
#[test]
fn parse_front_matter_numeric_title() {
let content = "---\ntitle: 42\n---\nBody";
let (fm, body) = parse_front_matter(content);
assert!(fm.title.is_none() || fm.title.is_some());
assert!(body.contains("Body"));
}
#[test]
fn parse_front_matter_multiline_body() {
let content = "---\ntitle: Test\n---\nLine 1\nLine 2\nLine 3";
let (fm, body) = parse_front_matter(content);
assert_eq!(fm.title.as_deref(), Some("Test"));
assert!(body.contains("Line 1"));
assert!(body.contains("Line 2"));
assert!(body.contains("Line 3"));
}
#[test]
fn parse_front_matter_only_body() {
let content = "No front matter at all, just plain text.";
let (fm, body) = parse_front_matter(content);
assert!(fm.title.is_none());
assert!(fm.tags.is_none());
assert!(fm.raw_yaml.is_none());
assert_eq!(body, content);
}
#[test]
fn parse_front_matter_tags_single_item_list() {
let content = "---\ntags:\n - solo\n---\nBody";
let (fm, _) = parse_front_matter(content);
assert_eq!(fm.tags.as_deref(), Some("solo"));
}
#[test]
fn parse_front_matter_many_fields() {
let content = "---\ntitle: My Doc\ntags:\n - a\n - b\n - c\n - d\nauthor: test\n---\nBody";
let (fm, body) = parse_front_matter(content);
assert_eq!(fm.title.as_deref(), Some("My Doc"));
assert_eq!(fm.tags.as_deref(), Some("a,b,c,d"));
assert!(fm.raw_yaml.is_some());
assert!(fm.raw_yaml.as_ref().unwrap().contains("author"));
assert!(body.contains("Body"));
}
#[test]
fn parse_front_matter_no_closing_delim() {
let content = "---\ntitle: Unclosed\nSome body text";
let (fm, body) = parse_front_matter(content);
assert!(fm.title.is_none() || fm.title.is_some());
assert!(!body.is_empty());
}
#[test]
fn matches_patterns_case_sensitive_extension() {
let result = matches_patterns(Path::new("FILE.MD"), &["*.md".to_string()]);
let _ = result;
}
#[test]
fn matches_patterns_deeply_nested_path() {
assert!(matches_patterns(
Path::new("a/b/c/d/e/f/g/h/note.md"),
&["*.md".to_string()]
));
}
#[test]
fn matches_patterns_no_extension() {
assert!(!matches_patterns(
Path::new("Makefile"),
&["*.md".to_string(), "*.txt".to_string()]
));
}
#[test]
fn matches_patterns_dot_file() {
assert!(matches_patterns(
Path::new(".hidden.md"),
&["*.md".to_string()]
));
}
#[test]
fn matches_patterns_question_mark_glob() {
assert!(matches_patterns(Path::new("a.md"), &["?.md".to_string()]));
assert!(!matches_patterns(Path::new("ab.md"), &["?.md".to_string()]));
}
#[test]
fn cooldown_set_many_paths() {
let mut cd = CooldownSet::new(Duration::from_secs(60));
for i in 0..100 {
cd.mark(std::path::PathBuf::from(format!("/test/file_{i}.md")));
}
assert_eq!(cd.entries.len(), 100);
for i in 0..100 {
assert!(cd.is_cooling(Path::new(&format!("/test/file_{i}.md"))));
}
}
#[test]
fn cooldown_set_zero_ttl_never_cools() {
let mut cd = CooldownSet::new(Duration::ZERO);
let path = std::path::PathBuf::from("/test/file.md");
cd.mark(path.clone());
assert!(!cd.is_cooling(&path));
}
#[test]
fn cooldown_set_cleanup_mixed_ages() {
let mut cd = CooldownSet::new(Duration::from_secs(5));
let old_path = std::path::PathBuf::from("/old.md");
let new_path = std::path::PathBuf::from("/new.md");
cd.entries
.insert(old_path.clone(), Instant::now() - Duration::from_secs(10));
cd.mark(new_path.clone());
cd.cleanup();
assert!(!cd.is_cooling(&old_path), "old entry should be cleaned");
assert!(cd.is_cooling(&new_path), "new entry should remain");
assert_eq!(cd.entries.len(), 1);
}
#[test]
fn relative_path_string_single_component() {
assert_eq!(relative_path_string(Path::new("notes")), "notes");
}
#[test]
fn relative_path_string_with_extension() {
assert_eq!(
relative_path_string(Path::new("sub/deep/file.txt")),
"sub/deep/file.txt"
);
}
#[test]
fn ingest_summary_with_multiple_errors() {
let summary = IngestSummary {
ingested: 10,
skipped: 5,
errors: vec![
"file1.md: parse error".to_string(),
"file2.txt: io error".to_string(),
"sub/file3.md: encoding error".to_string(),
],
};
assert_eq!(summary.ingested, 10);
assert_eq!(summary.skipped, 5);
assert_eq!(summary.errors.len(), 3);
assert!(summary.errors[0].contains("file1.md"));
assert!(summary.errors[1].contains("file2.txt"));
assert!(summary.errors[2].contains("sub/file3.md"));
}
#[test]
fn watchtower_error_io_from_std_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
let wt_err = WatchtowerError::Io(io_err);
let msg = wt_err.to_string();
assert!(msg.contains("IO error"));
assert!(msg.contains("access denied"));
}
#[test]
fn watchtower_error_config_empty_message() {
let err = WatchtowerError::Config(String::new());
assert_eq!(err.to_string(), "config error: ");
}
#[test]
fn watchtower_error_config_long_message() {
let long_msg = "x".repeat(500);
let err = WatchtowerError::Config(long_msg.clone());
let display = err.to_string();
assert!(display.contains(&long_msg));
}
#[test]
fn walk_directory_nested_hidden_dirs_skipped() {
let dir = tempfile::tempdir().expect("temp dir");
let base = dir.path();
let visible = base.join("visible");
std::fs::create_dir(&visible).expect("mkdir visible");
let hidden = visible.join(".git");
std::fs::create_dir(&hidden).expect("mkdir .git");
std::fs::write(hidden.join("config.md"), "git config").expect("write");
std::fs::write(visible.join("doc.md"), "doc").expect("write");
let mut out = Vec::new();
WatchtowerLoop::walk_directory(base, base, &["*.md".to_string()], &mut out).expect("walk");
assert_eq!(out.len(), 1);
assert_eq!(out[0], "visible/doc.md");
}
#[test]
fn walk_directory_multiple_patterns() {
let dir = tempfile::tempdir().expect("temp dir");
let base = dir.path();
std::fs::write(base.join("a.md"), "md").expect("write");
std::fs::write(base.join("b.txt"), "txt").expect("write");
std::fs::write(base.join("c.rs"), "rs").expect("write");
std::fs::write(base.join("d.md"), "md2").expect("write");
let mut out = Vec::new();
WatchtowerLoop::walk_directory(
base,
base,
&["*.md".to_string(), "*.txt".to_string()],
&mut out,
)
.expect("walk");
assert_eq!(out.len(), 3);
assert!(out.contains(&"a.md".to_string()));
assert!(out.contains(&"b.txt".to_string()));
assert!(out.contains(&"d.md".to_string()));
}
#[test]
fn walk_directory_deeply_nested_files() {
let dir = tempfile::tempdir().expect("temp dir");
let base = dir.path();
let deep = base.join("a").join("b").join("c");
std::fs::create_dir_all(&deep).expect("mkdir -p");
std::fs::write(deep.join("deep.md"), "deep file").expect("write");
let mut out = Vec::new();
WatchtowerLoop::walk_directory(base, base, &["*.md".to_string()], &mut out).expect("walk");
assert_eq!(out.len(), 1);
assert_eq!(out[0], "a/b/c/deep.md");
}
#[test]
fn parsed_front_matter_all_fields_set() {
let fm = ParsedFrontMatter {
title: Some("My Title".to_string()),
tags: Some("tag1,tag2,tag3".to_string()),
raw_yaml: Some("title: My Title\ntags: [tag1,tag2,tag3]".to_string()),
};
assert_eq!(fm.title.as_deref(), Some("My Title"));
assert_eq!(fm.tags.as_deref(), Some("tag1,tag2,tag3"));
assert!(fm.raw_yaml.as_ref().unwrap().contains("title"));
assert!(fm.raw_yaml.as_ref().unwrap().contains("tags"));
}
#[test]
fn parsed_front_matter_clone() {
let fm = ParsedFrontMatter {
title: Some("Clone Test".to_string()),
tags: None,
raw_yaml: None,
};
let debug = format!("{fm:?}");
assert!(debug.contains("Clone Test"));
}