use super::{GitRunner, ScaffoldError, TEMPLATE, TEMPLATE_OVERRIDE_DIR, scaffold};
use crate::harness_root::Roots;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
struct NoopGit;
impl GitRunner for NoopGit {
fn run(&self, _dest: &Path, _args: &[&str]) -> io::Result<()> {
Ok(())
}
fn run_capture(&self, dest: &Path, args: &[&str]) -> io::Result<String> {
self.run(dest, args).map(|_| String::new())
}
}
#[test]
fn noop_run_capture_delegates_to_run() {
assert_eq!(NoopGit.run_capture(Path::new("."), &["x"]).unwrap(), "");
}
fn embedded(rel: &str) -> &'static str {
std::str::from_utf8(TEMPLATE.get_file(rel).unwrap().contents()).unwrap()
}
struct Case {
name: &'static str,
overrides: &'static [(&'static str, &'static str)],
expect_exact: &'static [(&'static str, &'static str)],
expect_embedded: &'static [&'static str],
}
const CASES: &[Case] = &[
Case {
name: "override_wins_embedded_fallback",
overrides: &[("providers.yaml", "roles: {}\n")],
expect_exact: &[("providers.yaml", "roles: {}\n")],
expect_embedded: &["workflow.yaml", "manifest.yaml", "version"],
},
Case {
name: "extra_file_included",
overrides: &[("extra.yaml", "x: 1\n")],
expect_exact: &[("extra.yaml", "x: 1\n")],
expect_embedded: &["providers.yaml"],
},
Case {
name: "nested_override_wins",
overrides: &[("souls/worker.md", "# Overridden Worker\n")],
expect_exact: &[("souls/worker.md", "# Overridden Worker\n")],
expect_embedded: &["souls/compactor.md"],
},
Case {
name: "absent_dir_is_embedded_exactly",
overrides: &[],
expect_exact: &[],
expect_embedded: &[
"providers.yaml",
"workflow.yaml",
"manifest.yaml",
"version",
"souls/worker.md",
"souls/compactor.md",
],
},
];
#[test]
fn seed_set_is_union_with_override_winning() {
for case in CASES {
let holder = TempDir::new().unwrap();
let config = holder.path().join("conf");
for (rel, content) in case.overrides {
let p = config.join(TEMPLATE_OVERRIDE_DIR).join(rel);
fs::create_dir_all(p.parent().unwrap()).unwrap();
fs::write(&p, content).unwrap();
}
let roots = Roots {
config,
data: holder.path().join("no-pool"),
};
let dest = holder.path().join("ws");
scaffold(&dest, &roots, &NoopGit).unwrap();
let author = dest.join(".config-author");
for (rel, want) in case.expect_exact {
let got = fs::read_to_string(author.join(rel)).unwrap();
assert_eq!(&got, want, "{}: {rel}", case.name);
}
for rel in case.expect_embedded {
let got = fs::read_to_string(author.join(rel)).unwrap();
assert_eq!(got, embedded(rel), "{}: {rel}", case.name);
}
}
}
#[test]
fn override_path_that_is_a_file_surfaces_io() {
let holder = TempDir::new().unwrap();
let config = holder.path().join("conf");
fs::create_dir_all(&config).unwrap();
fs::write(config.join(TEMPLATE_OVERRIDE_DIR), b"not a dir").unwrap();
let roots = Roots {
config,
data: holder.path().join("no-pool"),
};
let err = scaffold(&holder.path().join("ws"), &roots, &NoopGit).unwrap_err();
assert!(matches!(err, ScaffoldError::Io(_)), "got {err:?}");
}
const INHERITED_GIT_ENV: &[&str] = &[
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_INDEX_FILE",
"GIT_OBJECT_DIRECTORY",
"GIT_PREFIX",
"GIT_COMMON_DIR",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
];
fn scrub_git_env(cmd: &mut Command) {
for var in INHERITED_GIT_ENV {
cmd.env_remove(var);
}
}
fn show_control(ws: &Path, path: &str) -> String {
let mut cmd = Command::new("git");
scrub_git_env(&mut cmd);
let out = cmd
.arg("-C")
.arg(ws.join("repo.git"))
.args(["show", &format!("config/default:{path}")])
.output()
.expect("spawn git show");
assert!(
out.status.success(),
"git show {path}: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap()
}
#[test]
fn lernie_home_collapse_reads_override_from_lernie_home_template() {
let over_providers = "roles:\n worker:\n provider: codex\n model: gpt-5.4\n";
let home = TempDir::new().unwrap();
let tmpl = home.path().join(TEMPLATE_OVERRIDE_DIR);
fs::create_dir_all(&tmpl).unwrap();
fs::write(tmpl.join("providers.yaml"), over_providers).unwrap();
fs::write(tmpl.join("extra.txt"), "extra\n").unwrap();
let holder = TempDir::new().unwrap();
let dest: PathBuf = holder.path().join("ws");
let mut cmd = Command::new(crate::test_support::lernie_binary());
scrub_git_env(&mut cmd);
let out = cmd
.arg("new")
.arg(&dest)
.env("LERNIE_HOME", home.path())
.env("GIT_AUTHOR_NAME", "lernie-test")
.env("GIT_AUTHOR_EMAIL", "test@example.invalid")
.env("GIT_COMMITTER_NAME", "lernie-test")
.env("GIT_COMMITTER_EMAIL", "test@example.invalid")
.env("GIT_CONFIG_COUNT", "1")
.env("GIT_CONFIG_KEY_0", "core.hooksPath")
.env("GIT_CONFIG_VALUE_0", "/dev/null")
.output()
.expect("invoke lernie binary");
assert!(
out.status.success(),
"lernie new failed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(show_control(&dest, "providers.yaml"), over_providers);
assert_eq!(show_control(&dest, "extra.txt"), "extra\n");
assert_eq!(
show_control(&dest, "workflow.yaml"),
embedded("workflow.yaml")
);
}