use brief::watch::{Engine, LlmOpts, Target, WatchOpts, handle_change_paths, scan_shortcode_uses};
use std::path::{Path, PathBuf};
use std::time::Duration;
fn temp_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("brief-watch-test-{}", name));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn opts_for(dir: &Path, target: Target) -> WatchOpts {
WatchOpts {
paths: vec![dir.to_path_buf()],
target,
config_path: dir.join("brief.toml"),
llm_opts: LlmOpts::default(),
no_clear: true,
}
}
fn read(p: &Path) -> String {
std::fs::read_to_string(p).expect(&format!("read {}", p.display()))
}
#[test]
fn brf_change_recompiles_only_that_file() {
let dir = temp_dir("brf-only-that-file");
let a = dir.join("a.brf");
let b = dir.join("b.brf");
std::fs::write(&a, "# A\n").unwrap();
std::fs::write(&b, "# B\n").unwrap();
let mut engine = Engine::load(&opts_for(&dir, Target::Html)).unwrap();
let mut log: Vec<u8> = Vec::new();
engine.compile_all(&mut log);
let a_html = dir.join("a.html");
let b_html = dir.join("b.html");
let b_before = read(&b_html);
std::fs::write(&a, "# A2\n").unwrap();
handle_change_paths(&[a.clone()], &mut engine, &mut log);
let a_after = read(&a_html);
assert!(a_after.contains("A2"), "a output not updated: {}", a_after);
assert_eq!(read(&b_html), b_before, "b should be untouched");
}
#[test]
fn brief_toml_structural_change_recompiles_all() {
let dir = temp_dir("toml-structural-all");
let a = dir.join("a.brf");
let b = dir.join("b.brf");
std::fs::write(&a, "# A\n").unwrap();
std::fs::write(&b, "# B\n").unwrap();
let mut engine = Engine::load(&opts_for(&dir, Target::Html)).unwrap();
let mut log: Vec<u8> = Vec::new();
engine.compile_all(&mut log);
let a_html = dir.join("a.html");
let b_html = dir.join("b.html");
let _a_before = read(&a_html);
let _b_before = read(&b_html);
std::fs::write(
dir.join("brief.toml"),
"[compile]\nstrict_heading_levels = true\n",
)
.unwrap();
handle_change_paths(&[dir.join("brief.toml")], &mut engine, &mut log);
let log_str = String::from_utf8_lossy(&log);
assert!(
log_str.contains("recompiling all"),
"expected 'recompiling all' in log, got:\n{}",
log_str
);
assert!(a_html.exists() && b_html.exists());
}
#[test]
fn template_only_change_recompiles_only_users() {
let dir = temp_dir("toml-template-users");
let cfg_path = dir.join("brief.toml");
std::fs::write(
&cfg_path,
r#"
[shortcodes.tip]
kind = "block"
template_html = "<aside class=\"v1\">{{content}}</aside>"
"#,
)
.unwrap();
let uses = dir.join("uses.brf");
let plain = dir.join("plain.brf");
std::fs::write(&uses, "@tip\nbody\n@end\n").unwrap();
std::fs::write(&plain, "# plain\n").unwrap();
let mut engine = Engine::load(&opts_for(&dir, Target::Html)).unwrap();
let mut log: Vec<u8> = Vec::new();
engine.compile_all(&mut log);
let uses_html = dir.join("uses.html");
let plain_html = dir.join("plain.html");
let v1 = read(&uses_html);
assert!(v1.contains("class=\"v1\""), "v1: {}", v1);
let plain_before = read(&plain_html);
let plain_mtime_before = std::fs::metadata(&plain_html).unwrap().modified().unwrap();
std::thread::sleep(Duration::from_millis(20));
std::fs::write(
&cfg_path,
r#"
[shortcodes.tip]
kind = "block"
template_html = "<aside class=\"v2\">{{content}}</aside>"
"#,
)
.unwrap();
log.clear();
handle_change_paths(&[cfg_path.clone()], &mut engine, &mut log);
let log_str = String::from_utf8_lossy(&log);
assert!(
log_str.contains("template change"),
"expected 'template change' in log, got:\n{}",
log_str
);
let v2 = read(&uses_html);
assert!(
v2.contains("class=\"v2\""),
"uses.html should reflect new template: {}",
v2
);
assert_eq!(
read(&plain_html),
plain_before,
"plain output content should be unchanged"
);
let plain_mtime_after = std::fs::metadata(&plain_html).unwrap().modified().unwrap();
assert_eq!(
plain_mtime_before, plain_mtime_after,
"plain output mtime should be unchanged"
);
}
#[test]
fn config_no_op_change_does_nothing() {
let dir = temp_dir("toml-noop");
let cfg_path = dir.join("brief.toml");
std::fs::write(&cfg_path, "[project]\nname = \"x\"\n").unwrap();
let a = dir.join("a.brf");
std::fs::write(&a, "# A\n").unwrap();
let mut engine = Engine::load(&opts_for(&dir, Target::Html)).unwrap();
let mut log: Vec<u8> = Vec::new();
engine.compile_all(&mut log);
let a_html = dir.join("a.html");
let mtime_before = std::fs::metadata(&a_html).unwrap().modified().unwrap();
std::thread::sleep(Duration::from_millis(20));
std::fs::write(&cfg_path, "[project]\nname = \"x\"\n").unwrap();
log.clear();
handle_change_paths(&[cfg_path.clone()], &mut engine, &mut log);
let log_str = String::from_utf8_lossy(&log);
assert!(
!log_str.contains("recompiling all"),
"no-op config change should not recompile, got:\n{}",
log_str
);
let mtime_after = std::fs::metadata(&a_html).unwrap().modified().unwrap();
assert_eq!(
mtime_before, mtime_after,
"a.html should not have been rewritten"
);
}
#[test]
fn deleted_brf_is_untracked() {
let dir = temp_dir("brf-deleted");
let a = dir.join("a.brf");
let b = dir.join("b.brf");
std::fs::write(&a, "# A\n").unwrap();
std::fs::write(&b, "# B\n").unwrap();
let mut engine = Engine::load(&opts_for(&dir, Target::Html)).unwrap();
let mut log: Vec<u8> = Vec::new();
engine.compile_all(&mut log);
assert_eq!(engine.files.len(), 2);
std::fs::remove_file(&a).unwrap();
handle_change_paths(&[a.clone()], &mut engine, &mut log);
assert_eq!(engine.files.len(), 1, "deleted file should be untracked");
}
#[test]
fn new_brf_is_picked_up() {
let dir = temp_dir("brf-new");
let a = dir.join("a.brf");
std::fs::write(&a, "# A\n").unwrap();
let mut engine = Engine::load(&opts_for(&dir, Target::Html)).unwrap();
let mut log: Vec<u8> = Vec::new();
engine.compile_all(&mut log);
assert_eq!(engine.files.len(), 1);
let b = dir.join("b.brf");
std::fs::write(&b, "# B\n").unwrap();
handle_change_paths(&[b.clone()], &mut engine, &mut log);
assert_eq!(engine.files.len(), 2);
assert!(dir.join("b.html").exists(), "b.html should be produced");
}
#[test]
fn llm_target_writes_txt_output() {
let dir = temp_dir("llm-output");
let a = dir.join("a.brf");
std::fs::write(&a, "# A\n").unwrap();
let mut engine = Engine::load(&opts_for(&dir, Target::Llm)).unwrap();
let mut log: Vec<u8> = Vec::new();
engine.compile_all(&mut log);
assert!(dir.join("a.txt").exists());
}
#[test]
fn scan_shortcode_uses_for_typical_brief_doc() {
let s = "@tip\nbody\n@end\n\n@link(\"u\") foo @kbd[c]\n";
let uses = scan_shortcode_uses(s);
assert!(uses.contains("tip"));
assert!(uses.contains("link"));
assert!(uses.contains("kbd"));
}