1 //! Conditional nav tabs (m6 hide-empty, f0rge issue #38):2 //! `tasks` / `releases` / `wiki` render in the header ONLY when non-empty;3 //! `issues` / `pulls` / `ci` / `code` / `commits` / `agents` always stay.4 //! Hidden sections still build (deep links keep working with their5 //! "No X yet" empty states); only the nav omits the tab.6 7 use std::sync::atomic::{AtomicU64, Ordering};8 9 static SEQ: AtomicU64 = AtomicU64::new(0);10 11 use std::path::{Path, PathBuf};12 use std::process::Command;13 14 fn bin() -> &'static str {15 env!("CARGO_BIN_EXE_hu13")16 }17 18 fn fresh(prefix: &str) -> PathBuf {19 let dir = std::env::temp_dir().join(format!(20 "hu13-hide-empty-{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 .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))27 ));28 std::fs::create_dir_all(&dir).unwrap();29 dir30 }31 32 fn git(repo: &Path, args: &[&str]) {33 let st = Command::new("git")34 .arg("-C")35 .arg(repo)36 .args(args)37 .status()38 .expect("spawn git");39 assert!(st.success(), "git {args:?} failed");40 }41 42 fn forge_write(repo: &Path, rel: &str, content: &str) {43 let dest = repo.join(".forge").join(rel);44 std::fs::create_dir_all(dest.parent().expect("parent")).unwrap();45 std::fs::write(&dest, content).unwrap();46 }47 48 fn bare_repo() -> PathBuf {49 let dir = fresh("repo");50 git(&dir, &["init", "-b", "main"]);51 git(&dir, &["config", "user.name", "hu13-test"]);52 git(&dir, &["config", "user.email", "[email protected]"]);53 git(&dir, &["config", "commit.gpgsign", "false"]);54 std::fs::write(dir.join("README.md"), "# hide-empty fixture\n").unwrap();55 git(&dir, &["add", "-A"]);56 git(&dir, &["commit", "-m", "init"]);57 dir58 }59 60 fn build(repo: &Path) -> PathBuf {61 let out = fresh("out");62 let st = Command::new(bin())63 .arg("build")64 .arg(repo)65 .arg("-o")66 .arg(&out)67 .status()68 .expect("spawn hu13 build");69 assert!(st.success(), "build exits 0");70 out71 }72 73 fn header_of(out: &Path, rel: &str) -> String {74 let html = std::fs::read_to_string(out.join(rel)).expect(rel);75 html.split("</header>")76 .next()77 .expect("header close")78 .to_string()79 }80 81 const ALWAYS_TABS: [&str; 6] = [82 "<a href=\"/\"",83 "<a href=\"/commits.html\"",84 "<a href=\"/issues/index.html\"",85 "<a href=\"/pulls/index.html\"",86 "<a href=\"/ci/index.html\"",87 "<a href=\"/agents.html\"",88 ];89 90 const CONDITIONAL_TABS: [(&str, &str); 3] = [91 ("tasks", "<a href=\"/tasks/index.html\""),92 ("releases", "<a href=\"/releases/index.html\""),93 ("wiki", "<a href=\"/wiki/index.html\""),94 ];95 96 #[test]97 fn empty_sections_hide_tabs_but_pages_still_build() {98 let out = build(&bare_repo());99 // Empty-state pages still build (deep links keep working).100 for (page, marker) in [101 ("tasks/index.html", "No tasks yet"),102 ("releases/index.html", "No releases yet"),103 ("wiki/index.html", "No wiki pages yet"),104 ] {105 let html = std::fs::read_to_string(out.join(page)).expect(page);106 assert!(html.contains(marker), "{page} keeps its empty state");107 }108 // Every sampled header hides the three tabs but keeps the rest.109 for page in ["index.html", "commits.html", "tasks/index.html"] {110 let header = header_of(&out, page);111 for (name, needle) in CONDITIONAL_TABS {112 assert!(!header.contains(needle), "{page} header hides empty {name}");113 }114 for needle in ALWAYS_TABS {115 assert!(116 header.contains(needle),117 "{page} header keeps always-shown {needle}"118 );119 }120 }121 }122 123 #[test]124 fn full_forge_shows_all_tabs() {125 let repo = bare_repo();126 forge_write(127 &repo,128 "tasks/77777777-7777-4777-8777-777777777777.md",129 "---\nid: 77777777-7777-4777-8777-777777777777\ntitle: A task\nstatus: todo\ncreated: 2026-09-10T03:00:00Z\n---\nTask body.\n",130 );131 forge_write(132 &repo,133 "releases/v1.0.md",134 "---\nversion: v1.0\ntitle: First\ncreated: 2026-09-10T02:00:00Z\n---\n# v1.0 notes\n",135 );136 forge_write(&repo, "wiki/notes.md", "# Notes\n\nwiki body\n");137 git(&repo, &["add", "-A"]);138 git(&repo, &["commit", "-m", "full forge"]);139 let out = build(&repo);140 let header = header_of(&out, "index.html");141 for (name, needle) in CONDITIONAL_TABS {142 assert!(143 header.contains(needle),144 "populated {name} tab shown in header"145 );146 }147 for needle in ALWAYS_TABS {148 assert!(header.contains(needle), "header keeps {needle}");149 }150 // Populated pages list their content.151 let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");152 assert!(tasks.contains("A task"), "task listed");153 let releases = std::fs::read_to_string(out.join("releases/index.html")).expect("releases");154 assert!(releases.contains("v1.0"), "release listed");155 let wiki = std::fs::read_to_string(out.join("wiki/index.html")).expect("wiki");156 assert!(wiki.contains("Notes"), "wiki page listed");157 }