1 //! dog-issue acceptance: quoting round-trip, empty-label drop,2 //! unknown-field warnings, archived default-hide on temp repos.3 4 use std::path::{Path, PathBuf};5 use std::process::Command;6 7 fn bin() -> Command {8 Command::new(env!("CARGO_BIN_EXE_f0rge"))9 }10 11 fn run_in(dir: &Path, args: &[&str]) -> (String, String, i32) {12 let out = bin().current_dir(dir).args(args).output().expect("run f0rge");13 (14 String::from_utf8_lossy(&out.stdout).into_owned(),15 String::from_utf8_lossy(&out.stderr).into_owned(),16 out.status.code().unwrap_or(-1),17 )18 }19 20 fn tmp(tag: &str) -> PathBuf {21 let d = std::env::temp_dir()22 .join(format!("f0rge-dogissue-{tag}-{}-{}", std::process::id(), line!()));23 let _ = std::fs::remove_dir_all(&d);24 std::fs::create_dir_all(&d).unwrap();25 d26 }27 28 fn setup(tag: &str) -> PathBuf {29 let dir = tmp(tag);30 let (_, _, code) = run_in(&dir, &["init"]);31 assert_eq!(code, 0);32 dir33 }34 35 fn jv(s: &str) -> serde_json::Value {36 serde_json::from_str(s.trim()).expect("json line")37 }38 39 #[test]40 fn label_quote_roundtrip() {41 let dir = setup("quote");42 let (_, err, code) = run_in(43 &dir,44 &["label", "create", "--name", "123", "--color", "5319E7", "--description", "true"],45 );46 assert_eq!(code, 0, "{err}");47 let raw = std::fs::read_to_string(dir.join(".forge/labels.md")).unwrap();48 assert!(raw.contains("\"123\""), "name quoted:\n{raw}");49 assert!(raw.contains("\"5319E7\""), "color quoted:\n{raw}");50 assert!(raw.contains("\"true\""), "desc quoted:\n{raw}");51 let (o, err, code) = run_in(&dir, &["label", "list", "--json"]);52 assert_eq!(code, 0, "{err}");53 let v = jv(&o);54 assert_eq!(v["name"], "123");55 assert_eq!(v["color"], "5319E7");56 assert_eq!(v["description"], "true");57 // plain names stay bare for humans58 let (_, _, code) = run_in(&dir, &["label", "create", "--name", "bug"]);59 assert_eq!(code, 0);60 let raw = std::fs::read_to_string(dir.join(".forge/labels.md")).unwrap();61 assert!(raw.contains("- name: bug\n"), "plain stays bare:\n{raw}");62 let _ = std::fs::remove_dir_all(&dir);63 }64 65 #[test]66 fn milestone_task_quote_roundtrip() {67 let dir = setup("mtquote");68 let (_, err, code) = run_in(&dir, &["milestone", "create", "--name", "007", "--due", "2026-10-01"]);69 assert_eq!(code, 0, "{err}");70 let (o, err, code) = run_in(&dir, &["milestone", "list", "--json"]);71 assert_eq!(code, 0, "{err}");72 assert_eq!(jv(&o)["name"], "007");73 let (_, err, code) = run_in(&dir, &["task", "create", "--title", "123", "--json"]);74 assert_eq!(code, 0, "{err}");75 let (o, _, code) = run_in(&dir, &["task", "list", "--json"]);76 assert_eq!(code, 0);77 assert_eq!(jv(&o)["title"], "123");78 let _ = std::fs::remove_dir_all(&dir);79 }80 81 #[test]82 fn issue_empty_labels_dropped() {83 let dir = setup("empty");84 let (o, _, code) = run_in(85 &dir,86 &["issue", "create", "--title", "T", "--label", "", "--label", "bug", "--assignee", ""],87 );88 assert_eq!(code, 0);89 let v = jv(&o);90 assert_eq!(v["labels"], serde_json::json!(["bug"]));91 assert_eq!(v["assignees"], serde_json::json!([]));92 // empty-only verbs are usage errors, one line93 let n = v["number"].to_string();94 let (_, err, code) = run_in(&dir, &["issue", "label", &n, "--label", ""]);95 assert_eq!(code, 2);96 assert_eq!(err.lines().count(), 1);97 let (_, err, code) = run_in(&dir, &["issue", "assign", &n, "--assignee", ""]);98 assert_eq!(code, 2);99 assert_eq!(err.lines().count(), 1);100 let _ = std::fs::remove_dir_all(&dir);101 }102 103 #[test]104 fn unknown_field_warns() {105 let dir = setup("warn");106 let (o, _, code) = run_in(&dir, &["issue", "create", "--title", "T", "--json"]);107 assert_eq!(code, 0);108 let uuid = {109 let issues = dir.join(".forge/issues");110 let e = std::fs::read_dir(&issues).unwrap().flatten().next().unwrap();111 e.file_name().to_string_lossy().into_owned()112 };113 let _ = o;114 let f = dir.join(".forge/issues").join(&uuid).join("issue.md");115 let t = std::fs::read_to_string(&f).unwrap().replace("state: open", "state: open\nxtra: 1");116 std::fs::write(&f, t).unwrap();117 let (_, err, code) = run_in(&dir, &["issue", "view", &uuid]);118 assert_eq!(code, 0);119 assert!(err.contains("unknown field"), "stderr:\n{err}");120 std::fs::write(dir.join(".forge/labels.md"), "- name: x\n bogus: 1\n").unwrap();121 let (_, err, code) = run_in(&dir, &["label", "list"]);122 assert_eq!(code, 0);123 assert!(err.contains("unknown field"), "stderr:\n{err}");124 let _ = std::fs::remove_dir_all(&dir);125 }126 127 #[test]128 fn archived_hidden_by_default() {129 let dir = setup("arch");130 let (o, _, code) = run_in(&dir, &["issue", "create", "--title", "A", "--json"]);131 assert_eq!(code, 0);132 let n = jv(&o)["number"].to_string();133 let (_, _, code) = run_in(&dir, &["issue", "close", &n]);134 assert_eq!(code, 0);135 // flip to archived on disk (no archive verb in scope)136 let issues = dir.join(".forge/issues");137 for e in std::fs::read_dir(&issues).unwrap().flatten() {138 let f = issues.join(e.file_name()).join("issue.md");139 let t = std::fs::read_to_string(&f).unwrap().replace("state: closed", "state: archived");140 std::fs::write(&f, t).unwrap();141 }142 let (o, _, code) = run_in(&dir, &["issue", "list", "--json"]);143 assert_eq!(code, 0);144 assert_eq!(o, "[]\n", "default hides archived, got:\n{o}");145 let (o, _, code) = run_in(&dir, &["issue", "list", "--json", "--state", "archived"]);146 assert_eq!(code, 0);147 assert_eq!(o.lines().count(), 1);148 let _ = std::fs::remove_dir_all(&dir);149 }