mod common;
use common::drft_bin;
use std::fs;
use tempfile::TempDir;
fn check(dir: &std::path::Path) -> String {
let output = drft_bin()
.args(["-C", dir.to_str().unwrap(), "check"])
.output()
.unwrap();
assert!(output.status.success(), "check should exit 0 on warnings");
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn lock(dir: &std::path::Path) {
let output = drft_bin()
.args(["-C", dir.to_str().unwrap(), "lock"])
.output()
.unwrap();
assert!(output.status.success(), "lock should exit 0");
}
#[test]
fn first_lock_then_clean_check() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("drft.toml"), common::DEFAULT_CONFIG).unwrap();
fs::write(dir.path().join("index.md"), "[setup](setup.md)").unwrap();
fs::write(dir.path().join("setup.md"), "# Setup").unwrap();
lock(dir.path());
let lockfile = fs::read_to_string(dir.path().join("drft.lock")).unwrap();
assert!(lockfile.contains("[[node]]"));
assert!(lockfile.contains("path = \"index.md\""));
assert!(lockfile.contains("[[node.edge]]"));
assert!(lockfile.contains("target = \"setup.md\""));
assert!(lockfile.contains("b3:"));
assert!(
!lockfile.contains("version"),
"lockfile should carry no version field"
);
assert!(
!lockfile.contains("path = \"drft.lock\""),
"the lockfile should not list itself as a node"
);
let stdout = check(dir.path());
assert!(
!stdout.contains("stale"),
"expected no staleness after lock, got: {stdout}"
);
}
#[test]
fn edit_dependency_reports_stale_node_and_stale_edge() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("drft.toml"), common::DEFAULT_CONFIG).unwrap();
fs::write(dir.path().join("index.md"), "[setup](setup.md)").unwrap();
fs::write(dir.path().join("setup.md"), "# Setup").unwrap();
lock(dir.path());
fs::write(dir.path().join("setup.md"), "# Setup (edited)").unwrap();
let stdout = check(dir.path());
assert!(
stdout.contains("warn[stale-node]: setup.md"),
"expected stale-node on the edited file, got: {stdout}"
);
assert!(
stdout.contains("warn[stale-edge]: index.md"),
"expected stale-edge on the dependent, got: {stdout}"
);
}
#[test]
fn relock_clears_staleness() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("drft.toml"), common::DEFAULT_CONFIG).unwrap();
fs::write(dir.path().join("index.md"), "[setup](setup.md)").unwrap();
fs::write(dir.path().join("setup.md"), "# Setup").unwrap();
lock(dir.path());
fs::write(dir.path().join("setup.md"), "# Setup (edited)").unwrap();
assert!(check(dir.path()).contains("stale"));
lock(dir.path());
assert!(
!check(dir.path()).contains("stale"),
"re-locking should clear staleness"
);
}
#[test]
fn deleted_file_reports_unresolved_and_removed_node() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("drft.toml"), common::DEFAULT_CONFIG).unwrap();
fs::write(dir.path().join("index.md"), "[setup](setup.md)").unwrap();
fs::write(dir.path().join("setup.md"), "# Setup").unwrap();
lock(dir.path());
fs::remove_file(dir.path().join("setup.md")).unwrap();
let stdout = check(dir.path());
assert!(
stdout.contains("unresolved-edge"),
"expected unresolved-edge, got: {stdout}"
);
assert!(
stdout.contains("removed-node"),
"expected removed-node, got: {stdout}"
);
}
#[test]
fn scoped_lock_accepts_several_paths() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("drft.toml"), common::DEFAULT_CONFIG).unwrap();
for name in ["a", "b", "c"] {
fs::write(dir.path().join(format!("{name}.md")), format!("# {name}")).unwrap();
}
lock(dir.path());
for name in ["a", "b", "c"] {
fs::write(
dir.path().join(format!("{name}.md")),
format!("# {name} edited"),
)
.unwrap();
}
let output = drft_bin()
.args(["-C", dir.path().to_str().unwrap(), "lock", "a.md", "b.md"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = check(dir.path());
assert!(!stdout.contains("stale-node]: a.md"), "got: {stdout}");
assert!(!stdout.contains("stale-node]: b.md"), "got: {stdout}");
assert!(
stdout.contains("stale-node]: c.md"),
"c.md was not named and must stay stale, got: {stdout}"
);
}
#[test]
fn scoped_lock_writes_nothing_when_a_path_is_unresolvable() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("drft.toml"), common::DEFAULT_CONFIG).unwrap();
fs::write(dir.path().join("a.md"), "# a").unwrap();
lock(dir.path());
fs::write(dir.path().join("a.md"), "# a edited").unwrap();
let output = drft_bin()
.args([
"-C",
dir.path().to_str().unwrap(),
"lock",
"a.md",
"typo.md",
])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(2), "unresolvable path exits 2");
let stdout = check(dir.path());
assert!(
stdout.contains("stale-node]: a.md"),
"a.md must not be locked when a later path fails, got: {stdout}"
);
}