#![allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::tests_outside_test_module,
reason = "integration test: fail-fast unwrap/expect are idiomatic, and test fns live at crate root by construction"
)]
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
const BIN: &str = env!("CARGO_BIN_EXE_doctrine");
fn git(dir: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.expect("spawn git");
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn git_try(dir: &Path, args: &[&str]) -> (bool, String) {
let out = Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.expect("spawn git");
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).trim().to_string(),
)
}
fn init_repo(dir: &Path) {
std::fs::create_dir_all(dir).unwrap();
git(dir, &["init", "-q", "-b", "main"]);
git(dir, &["config", "user.email", "t@example.com"]);
git(dir, &["config", "user.name", "Test"]);
std::fs::create_dir_all(dir.join(".doctrine")).unwrap();
std::fs::write(dir.join("a.txt"), "hello").unwrap();
git(dir, &["add", "."]);
git(dir, &["commit", "-q", "-m", "base"]);
}
fn stamp_marker(root: &Path) {
let dir = root.join(".doctrine/state/dispatch");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("worker"), b"").unwrap();
}
fn run(cwd: &Path, worker: Option<bool>, args: &[&str]) -> Output {
let mut cmd = Command::new(BIN);
cmd.args(args).current_dir(cwd);
match worker {
Some(true) => {
cmd.env("DOCTRINE_WORKER", "1");
}
Some(false) | None => {
cmd.env_remove("DOCTRINE_WORKER");
}
}
cmd.output().expect("spawn doctrine")
}
fn stdout(out: &Output) -> String {
String::from_utf8(out.stdout.clone()).expect("utf8 stdout")
}
fn stderr(out: &Output) -> String {
String::from_utf8(out.stderr.clone()).expect("utf8 stderr")
}
fn make_fork_branch(src: &Path, holder: &Path, branch: &str, files: &[(&str, &str)]) -> String {
let base = git(src, &["rev-parse", "HEAD"]);
let wt = holder.join(branch);
git(
src,
&["worktree", "add", "-b", branch, wt.to_str().unwrap(), &base],
);
for (rel, body) in files {
let p = wt.join(rel);
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&p, body).unwrap();
git(&wt, &["add", "-f", rel]);
}
git(&wt, &["commit", "-q", "-m", &format!("S: {branch}")]);
base
}
#[test]
fn import_happy_stages_delta_uncommitted() {
let src = tempfile::tempdir().unwrap();
init_repo(src.path());
let holder = tempfile::tempdir().unwrap();
let base = make_fork_branch(
src.path(),
holder.path(),
"wkr-1",
&[("feature.rs", "fn f() {}")],
);
let out = run(
src.path(),
None,
&["worktree", "import", "--base", &base, "--fork", "wkr-1"],
);
assert!(
out.status.success(),
"happy import must succeed; stderr: {}",
stderr(&out)
);
assert_eq!(
git(src.path(), &["rev-parse", "HEAD"]),
base,
"import must NOT move HEAD"
);
let staged = git(src.path(), &["diff", "--cached", "--name-only"]);
assert!(
staged.lines().any(|l| l == "feature.rs"),
"delta staged in the index; got: {staged:?}"
);
let (ok, _) = git_try(
src.path(),
&["cat-file", "-e", &format!("{base}:feature.rs")],
);
assert!(!ok, "feature.rs must not exist in the B commit");
}
fn assert_refusal(out: &Output, token: &str) {
assert!(
!out.status.success(),
"must refuse ({token}); stdout: {}, stderr: {}",
stdout(out),
stderr(out)
);
assert!(
stderr(out).contains(token),
"refusal names `{token}`; stderr: {}",
stderr(out)
);
}
#[test]
fn import_refuses_head_moved() {
let src = tempfile::tempdir().unwrap();
init_repo(src.path());
let holder = tempfile::tempdir().unwrap();
let base = make_fork_branch(src.path(), holder.path(), "wkr-hm", &[("f.rs", "x")]);
std::fs::write(src.path().join("drift.txt"), "drift").unwrap();
git(src.path(), &["add", "drift.txt"]);
git(src.path(), &["commit", "-q", "-m", "head moves off B"]);
let out = run(
src.path(),
None,
&["worktree", "import", "--base", &base, "--fork", "wkr-hm"],
);
assert_refusal(&out, "head-moved");
}
#[test]
fn import_refuses_tree_unclean() {
let src = tempfile::tempdir().unwrap();
init_repo(src.path());
let holder = tempfile::tempdir().unwrap();
let base = make_fork_branch(src.path(), holder.path(), "wkr-tu", &[("f.rs", "x")]);
std::fs::write(src.path().join("a.txt"), "dirtied").unwrap();
let out = run(
src.path(),
None,
&["worktree", "import", "--base", &base, "--fork", "wkr-tu"],
);
assert_refusal(&out, "tree-unclean");
}
#[test]
fn import_refuses_multi_commit() {
let src = tempfile::tempdir().unwrap();
init_repo(src.path());
let base = git(src.path(), &["rev-parse", "HEAD"]);
let holder = tempfile::tempdir().unwrap();
let wt = holder.path().join("wkr-mc");
git(
src.path(),
&[
"worktree",
"add",
"-b",
"wkr-mc",
wt.to_str().unwrap(),
&base,
],
);
std::fs::write(wt.join("one.rs"), "1").unwrap();
git(&wt, &["add", "one.rs"]);
git(&wt, &["commit", "-q", "-m", "first"]);
std::fs::write(wt.join("two.rs"), "2").unwrap();
git(&wt, &["add", "two.rs"]);
git(&wt, &["commit", "-q", "-m", "second"]);
let out = run(
src.path(),
None,
&["worktree", "import", "--base", &base, "--fork", "wkr-mc"],
);
assert_refusal(&out, "multi-commit");
}
#[test]
fn import_refuses_doctrine_touch() {
let src = tempfile::tempdir().unwrap();
init_repo(src.path());
let holder = tempfile::tempdir().unwrap();
let base = make_fork_branch(
src.path(),
holder.path(),
"wkr-dt",
&[(".doctrine/state/sneaky.txt", "nope")],
);
let out = run(
src.path(),
None,
&["worktree", "import", "--base", &base, "--fork", "wkr-dt"],
);
assert_refusal(&out, "doctrine-touch");
}
#[test]
fn import_refuses_claude_touch() {
let src = tempfile::tempdir().unwrap();
init_repo(src.path());
let holder = tempfile::tempdir().unwrap();
let base = make_fork_branch(
src.path(),
holder.path(),
"wkr-ct",
&[(".claude/settings.json", "{}")],
);
let out = run(
src.path(),
None,
&["worktree", "import", "--base", &base, "--fork", "wkr-ct"],
);
assert_refusal(&out, "claude-touch");
}
#[test]
fn import_refuses_doctrine_touch_quoted_path() {
let src = tempfile::tempdir().unwrap();
init_repo(src.path());
let holder = tempfile::tempdir().unwrap();
let base = make_fork_branch(
src.path(),
holder.path(),
"wkr-qp",
&[(".doctrine/é vil.toml", "nope")],
);
let out = run(
src.path(),
None,
&["worktree", "import", "--base", &base, "--fork", "wkr-qp"],
);
assert_refusal(&out, "doctrine-touch");
}
#[test]
fn import_refuses_rename_disguised_doctrine_deletion() {
let src = tempfile::tempdir().unwrap();
init_repo(src.path());
let gov_body = "GOVERNANCE\n".repeat(40);
std::fs::create_dir_all(src.path().join(".doctrine/state")).unwrap();
std::fs::write(src.path().join(".doctrine/state/gov.txt"), &gov_body).unwrap();
git(src.path(), &["add", "-f", ".doctrine/state/gov.txt"]);
git(src.path(), &["commit", "-q", "-m", "seed governance file"]);
let base = git(src.path(), &["rev-parse", "HEAD"]);
let holder = tempfile::tempdir().unwrap();
let wt = holder.path().join("wkr-rn");
git(
src.path(),
&[
"worktree",
"add",
"-b",
"wkr-rn",
wt.to_str().unwrap(),
&base,
],
);
std::fs::remove_file(wt.join(".doctrine/state/gov.txt")).unwrap();
std::fs::create_dir_all(wt.join("moved")).unwrap();
std::fs::write(wt.join("moved/gov.txt"), &gov_body).unwrap();
git(&wt, &["add", "-A"]);
git(
&wt,
&["commit", "-q", "-m", "S: rename-disguised gov deletion"],
);
let out = run(
src.path(),
None,
&["worktree", "import", "--base", &base, "--fork", "wkr-rn"],
);
assert_refusal(&out, "doctrine-touch");
}
#[test]
fn import_ignores_untracked_scratch() {
let src = tempfile::tempdir().unwrap();
init_repo(src.path());
let holder = tempfile::tempdir().unwrap();
let base = make_fork_branch(src.path(), holder.path(), "wkr-ut", &[("g.rs", "ok")]);
std::fs::write(src.path().join("scratch.tmp"), "ephemeral").unwrap();
let out = run(
src.path(),
None,
&["worktree", "import", "--base", &base, "--fork", "wkr-ut"],
);
assert!(
out.status.success(),
"untracked scratch must NOT trip tree-unclean; stderr: {}",
stderr(&out)
);
let staged = git(src.path(), &["diff", "--cached", "--name-only"]);
assert!(
staged.lines().any(|l| l == "g.rs"),
"delta still staged; got: {staged:?}"
);
}
fn add_linked_fork(src: &Path, holder: &Path, branch: &str) -> PathBuf {
let base = git(src, &["rev-parse", "HEAD"]);
let fork = holder.join("linked");
git(
src,
&[
"worktree",
"add",
"-b",
branch,
fork.to_str().unwrap(),
&base,
],
);
fork
}
#[test]
fn import_refused_under_worker_mode() {
let src = tempfile::tempdir().unwrap();
init_repo(src.path());
let base = git(src.path(), &["rev-parse", "HEAD"]);
let holder = tempfile::tempdir().unwrap();
let fork = add_linked_fork(src.path(), holder.path(), "wkr-guard");
stamp_marker(&fork);
let out = run(
&fork,
None,
&["worktree", "import", "--base", &base, "--fork", "wkr-guard"],
);
assert!(
!out.status.success(),
"import refused from a marked linked worktree; stdout: {}",
stdout(&out)
);
assert!(
stderr(&out).contains("import"),
"refusal names the verb; stderr: {}",
stderr(&out)
);
let out = run(
src.path(),
Some(true),
&["worktree", "import", "--base", &base, "--fork", "wkr-guard"],
);
assert!(
!out.status.success(),
"import refused when DOCTRINE_WORKER set; stdout: {}",
stdout(&out)
);
assert!(
stderr(&out).contains("DOCTRINE_WORKER"),
"env carries the dual-cause; stderr: {}",
stderr(&out)
);
}