use std::path::{Path, PathBuf};
use std::process::Command;
const BIN: &str = env!("CARGO_BIN_EXE_roteiro");
fn git(dir: &Path, args: &[&str]) -> std::process::Output {
let out = Command::new("git")
.args([
"-c",
"user.name=Test",
"-c",
"user.email=test@example.com",
"-c",
"commit.gpgsign=false",
"-c",
"init.defaultBranch=main",
])
.args(args)
.current_dir(dir)
.output()
.expect("run git");
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
out
}
fn roteiro(dir: &Path, args: &[&str]) -> std::process::Output {
Command::new(BIN)
.args(args)
.current_dir(dir)
.output()
.expect("run roteiro")
}
fn write(dir: &Path, rel: &str, content: &str) {
let path = dir.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).expect("mkdir");
std::fs::write(path, content).expect("write");
}
fn adr(id: &str, title: &str) -> String {
format!("---\nadr-id: \"{id}\"\nstatus: Accepted\n---\n\n# {title}\n\n## Decision\n\nBody.\n")
}
fn fresh_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("roteiro-wt-cli-{}-{name}", std::process::id()));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(&dir).expect("mkdir");
dir
}
fn checked_adrs(out: &std::process::Output) -> usize {
let stdout = String::from_utf8_lossy(&out.stdout);
let line = stdout
.lines()
.find(|l| l.starts_with("checked "))
.unwrap_or_else(|| panic!("no `checked …` line in: {stdout}"));
line.trim_start_matches("checked ")
.split(' ')
.next()
.and_then(|n| n.parse().ok())
.unwrap_or_else(|| panic!("cannot parse count from: {line}"))
}
#[test]
fn linked_worktrees_keep_separate_graphs_but_share_the_extraction_cache() {
let base = fresh_dir("isolation");
let main = base.join("main");
let side = base.join("side");
std::fs::create_dir_all(&main).expect("mkdir main");
git(&main, &["init", "-q"]);
write(&main, "src/lib.rs", "pub struct Thing;\n");
write(&main, "docs/adr/0001-one.md", &adr("0001", "One"));
git(&main, &["add", "."]);
git(&main, &["commit", "-q", "-m", "init"]);
git(
&main,
&[
"worktree",
"add",
"-q",
side.to_str().unwrap(),
"-b",
"side",
],
);
write(&side, "docs/adr/0002-two.md", &adr("0002", "Two"));
git(&side, &["add", "."]);
git(&side, &["commit", "-q", "-m", "second adr"]);
assert!(roteiro(&main, &["sync"]).status.success(), "main sync");
assert!(roteiro(&side, &["sync"]).status.success(), "side sync");
let main_check = roteiro(&main, &["check", "--committed"]);
let side_check = roteiro(&side, &["check", "--committed"]);
assert!(main_check.status.success(), "main check");
assert!(side_check.status.success(), "side check");
assert_eq!(
checked_adrs(&main_check),
1,
"the main tree has one ADR and must report one, whoever synced last: {}",
String::from_utf8_lossy(&main_check.stdout)
);
assert_eq!(
checked_adrs(&side_check),
2,
"the side tree has two ADRs: {}",
String::from_utf8_lossy(&side_check.stdout)
);
let main_db = main.join(".git/roteiro/graph.db");
let side_db = main.join(".git/worktrees/side/roteiro/graph.db");
assert!(main_db.is_file(), "main graph.db at {}", main_db.display());
assert!(side_db.is_file(), "side graph.db at {}", side_db.display());
assert!(
main.join(".git/roteiro/objects").is_dir(),
"shared object cache under the common git dir"
);
assert!(
!main.join(".git/worktrees/side/roteiro/objects").exists(),
"the object cache must NOT be duplicated per worktree — it is \
content-addressed, so extraction done in one tree is reusable in all"
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn a_store_holding_another_worktree_is_rebuilt_and_says_so() {
let base = fresh_dir("foreign");
let main = base.join("main");
let side = base.join("side");
std::fs::create_dir_all(&main).expect("mkdir main");
git(&main, &["init", "-q"]);
write(&main, "src/lib.rs", "pub struct Thing;\n");
write(&main, "docs/adr/0001-one.md", &adr("0001", "One"));
git(&main, &["add", "."]);
git(&main, &["commit", "-q", "-m", "init"]);
git(
&main,
&[
"worktree",
"add",
"-q",
side.to_str().unwrap(),
"-b",
"side",
],
);
write(&side, "docs/adr/0002-two.md", &adr("0002", "Two"));
write(&side, "docs/adr/0003-three.md", &adr("0003", "Three"));
git(&side, &["add", "."]);
git(&side, &["commit", "-q", "-m", "more adrs"]);
assert!(roteiro(&main, &["sync"]).status.success(), "main sync");
assert!(roteiro(&side, &["sync"]).status.success(), "side sync");
std::fs::copy(
main.join(".git/worktrees/side/roteiro/graph.db"),
main.join(".git/roteiro/graph.db"),
)
.expect("plant foreign store");
let out = roteiro(&main, &["sync"]);
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(
out.status.success(),
"sync should recover, not fail: {stderr}"
);
assert!(
!stdout.contains("up to date"),
"a store from another working tree must never short-circuit as \
`up to date`: {stdout}"
);
assert!(
stderr.contains("different working tree"),
"the rebuild must be announced: {stderr}"
);
assert!(
stderr.contains(side.to_string_lossy().as_ref()),
"the message must name the tree the store was holding ({}): {stderr}",
side.display()
);
let check = roteiro(&main, &["check", "--committed"]);
assert_eq!(
checked_adrs(&check),
1,
"the rebuilt graph describes the main tree: {}",
String::from_utf8_lossy(&check.stdout)
);
let again = roteiro(&main, &["sync"]);
assert!(
String::from_utf8_lossy(&again.stdout).contains("up to date"),
"once adopted, the store is ours: {}",
String::from_utf8_lossy(&again.stdout)
);
assert!(
!String::from_utf8_lossy(&again.stderr).contains("different working tree"),
"the notice must not repeat once the store has been adopted"
);
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn a_new_untracked_adr_is_checked_not_silently_skipped() {
let dir = fresh_dir("untracked-adr");
git(&dir, &["init", "-q"]);
write(&dir, "src/lib.rs", "pub struct Thing;\n");
write(&dir, "docs/adr/0001-one.md", &adr("0001", "One"));
git(&dir, &["add", "."]);
git(&dir, &["commit", "-q", "-m", "init"]);
let committed = roteiro(&dir, &["check"]);
assert_eq!(checked_adrs(&committed), 1, "baseline: one ADR");
write(&dir, "docs/adr/0002-two.md", &adr("0002", "Two"));
let out = roteiro(&dir, &["check"]);
assert!(out.status.success(), "check should still pass");
assert_eq!(
checked_adrs(&out),
2,
"the working-tree check must see the ADR that is on disk — reporting 1 \
while 2 exist is the silent wrong answer of #330: {}",
String::from_utf8_lossy(&out.stdout)
);
assert_eq!(
checked_adrs(&roteiro(&dir, &["check", "--committed"])),
1,
"--committed still describes HEAD only"
);
write(&dir, "docs/adr/0002b-dupe.md", &adr("0002", "Two again"));
let dupe = roteiro(&dir, &["check"]);
assert!(
!dupe.status.success(),
"a duplicate id in an untracked draft must fail: {}",
String::from_utf8_lossy(&dupe.stdout)
);
assert!(
String::from_utf8_lossy(&dupe.stderr).contains("duplicate-adr-id"),
"reported as a duplicate id: {}",
String::from_utf8_lossy(&dupe.stderr)
);
std::fs::remove_dir_all(&dir).ok();
}