diff --git a/tests/clean_out.rs b/tests/clean_out.rsnew file mode 100644index 0000000..5288f56--- /dev/null+++ b/tests/clean_out.rs@@ -0,0 +1,119 @@1+//! f0rge #67: a reused `-o DIR` must not keep serving pages whose source2+//! (issue, file, release) was deleted. Every build starts from an empty3+//! output dir, and the guard refuses to delete the repo or its ancestors.4+5+use std::path::{Path, PathBuf};6+use std::process::Command;7+8+fn git(repo: &Path, args: &[&str]) {9+ let st = Command::new("git")10+ .arg("-C")11+ .arg(repo)12+ .args(args)13+ .status()14+ .expect("spawn git");15+ assert!(st.success(), "git {args:?} failed");16+}17+18+fn fresh_dir(prefix: &str) -> PathBuf {19+ let dir = std::env::temp_dir().join(format!(20+ "hu13-clean-{prefix}-{}-{}",21+ std::process::id(),22+ std::time::SystemTime::now()23+ .duration_since(std::time::UNIX_EPOCH)24+ .map(|d| d.as_nanos())25+ .unwrap_or(0)26+ ));27+ std::fs::create_dir_all(&dir).unwrap();28+ dir29+}30+31+fn build(repo: &Path, out: &Path) -> std::process::Output {32+ Command::new(env!("CARGO_BIN_EXE_hu13"))33+ .arg("build")34+ .arg(repo)35+ .arg("-o")36+ .arg(out)37+ .output()38+ .expect("spawn hu13 build")39+}40+41+#[test]42+fn rebuild_removes_stale_pages() {43+ let repo = fresh_dir("repo");44+ let out = fresh_dir("out");45+ git(&repo, &["init", "-b", "main"]);46+ git(&repo, &["config", "user.name", "hu13-test"]);47+ git(&repo, &["config", "user.email", "[email protected]"]);48+ git(&repo, &["config", "commit.gpgsign", "false"]);49+ std::fs::write(repo.join("README.md"), "# clean\n").unwrap();50+ std::fs::write(repo.join("keep.txt"), "keep\n").unwrap();51+ let issue = repo.join(".forge/issues/99999999-9999-4999-8999-999999999999");52+ std::fs::create_dir_all(&issue).unwrap();53+ std::fs::write(54+ issue.join("issue.md"),55+ "---\nid: 99999999-9999-4999-8999-999999999999\ntitle: gone soon\n\56+ state: open\ncreated: 2026-09-10T03:00:00Z\n---\nbody\n",57+ )58+ .unwrap();59+ git(&repo, &["add", "-A"]);60+ git(&repo, &["commit", "-m", "one"]);61+62+ let first = build(&repo, &out);63+ assert!(64+ first.status.success(),65+ "first build: {}",66+ String::from_utf8_lossy(&first.stderr)67+ );68+ let page = out.join("issues/99999999-9999-4999-8999-999999999999.html");69+ assert!(page.is_file(), "issue page written");70+ assert!(out.join("raw/keep.txt").is_file());71+72+ // Delete the issue + a file, rebuild into the same out dir.73+ std::fs::remove_dir_all(repo.join(".forge/issues/99999999-9999-4999-8999-999999999999"))74+ .unwrap();75+ std::fs::remove_file(repo.join("keep.txt")).unwrap();76+ std::fs::write(repo.join("README.md"), "# clean two\n").unwrap();77+ git(&repo, &["add", "-A"]);78+ git(&repo, &["commit", "-m", "two"]);79+80+ let second = build(&repo, &out);81+ assert!(82+ second.status.success(),83+ "second build: {}",84+ String::from_utf8_lossy(&second.stderr)85+ );86+ assert!(!page.exists(), "stale issue page must be gone");87+ assert!(88+ !out.join("raw/keep.txt").exists(),89+ "stale raw file must be gone"90+ );91+ assert!(out.join("raw/README.md").is_file(), "fresh output present");92+}93+94+#[test]95+fn refuses_to_clean_repo_or_its_parent() {96+ let repo = fresh_dir("repo");97+ git(&repo, &["init", "-b", "main"]);98+ git(&repo, &["config", "user.name", "hu13-test"]);99+ git(&repo, &["config", "user.email", "[email protected]"]);100+ git(&repo, &["config", "commit.gpgsign", "false"]);101+ std::fs::write(repo.join("README.md"), "# keep\n").unwrap();102+ git(&repo, &["add", "-A"]);103+ git(&repo, &["commit", "-m", "one"]);104+ let sender = repo.join("src.txt");105+ std::fs::write(&sender, "must survive\n").unwrap();106+107+ for out in [repo.clone(), repo.parent().unwrap().to_path_buf()] {108+ let res = build(&repo, &out);109+ assert!(110+ !res.status.success(),111+ "must refuse to clean {}",112+ out.display()113+ );114+ let err = String::from_utf8_lossy(&res.stderr);115+ assert!(err.contains("refusing"), "stderr: {err}");116+ assert!(repo.join("README.md").is_file(), "repo untouched");117+ assert!(sender.is_file(), "repo untouched");118+ }119+}