use super::*;
use crate::template::RealGit;
use crate::workspace::{config_ref, fixture, proposal::proposal_ref, repo_git};
use std::path::PathBuf;
use tempfile::TempDir;
fn workspace_with_a_proposal(id: &str) -> (TempDir, PathBuf, String) {
staged_proposal(id, "notes: record the lesson")
}
fn staged_proposal(id: &str, message: &str) -> (TempDir, PathBuf, String) {
let (h, ws) = fixture::workspace();
let git = RealGit::new();
let repo = repo_git(&ws);
let parent = git
.run_capture(&repo, &["rev-parse", &config_ref("default")])
.unwrap();
let parent = parent.trim().to_string();
let scratch = h.path().join("mint");
let scratch_s = scratch.to_string_lossy().into_owned();
git.run(
&repo,
&[
"worktree",
"add",
"-b",
&proposal_ref(id),
&scratch_s,
&parent,
],
)
.unwrap();
std::fs::create_dir_all(scratch.join("skills/notes")).unwrap();
std::fs::write(
scratch.join("skills/notes/SKILL.md"),
"---\nname: notes\ndescription: d\n---\nthe lesson\n",
)
.unwrap();
git.run(&scratch, &["add", "-A"]).unwrap();
git.run(&scratch, &["commit", "-m", message]).unwrap();
git.run(&repo, &["worktree", "remove", "--force", &scratch_s])
.unwrap();
(h, ws, parent)
}
#[test]
fn a_subject_is_derived_past_the_ornament_a_response_opens_with() {
let (_h, ws, _p) = staged_proposal(
"a1-r1",
"## Final Response\n\nnotes: record the correction\n\nbecause the user said so\n",
);
let rows = list(&ws, &RealGit::new()).unwrap();
assert_eq!(rows[0].subject, "notes: record the correction");
}
#[test]
fn a_message_with_no_subject_in_it_lists_the_paths_that_change() {
let (_h, ws, _p) = staged_proposal("a1-r2", "---\n\n***\n");
let rows = list(&ws, &RealGit::new()).unwrap();
assert_eq!(rows[0].subject, "skills/notes/SKILL.md");
}
fn head(ws: &Path, r: &str) -> Option<String> {
RealGit::new()
.run_capture(&repo_git(ws), &["rev-parse", "--verify", r])
.ok()
.map(|s| s.trim().to_string())
}
#[test]
fn a_proposal_lists_fresh_against_the_lineage_it_stands_on() {
let (_h, ws, parent) = workspace_with_a_proposal("a1-r1");
let rows = list(&ws, &RealGit::new()).unwrap();
assert_eq!(rows.len(), 1);
let row = &rows[0];
assert_eq!(row.id, "a1-r1");
assert_eq!(row.lineages, vec!["default".to_string()]);
assert!(row.fresh, "the parent is still the head");
assert_eq!(row.parent, short(&parent));
assert_eq!(row.subject, "notes: record the lesson");
assert!(row.diffstat.contains("1 file changed"), "{}", row.diffstat);
}
#[test]
fn a_config_advance_makes_the_same_proposal_stale_with_no_field_rewritten() {
let (_h, ws, _parent) = workspace_with_a_proposal("a1-r2");
fixture::amend_config(&ws, &[("souls/worker.md", "a new soul")]);
let rows = list(&ws, &RealGit::new()).unwrap();
assert!(!rows[0].fresh, "the head moved out from under it");
assert!(rows[0].lineages.is_empty(), "no lineage stands there now");
}
#[test]
fn accept_fast_forwards_the_lineage_and_deletes_the_branch() {
let (_h, ws, _parent) = workspace_with_a_proposal("a1-r3");
let git = RealGit::new();
let staged = head(&ws, &proposal_ref("a1-r3")).unwrap();
let line = accept(&ws, "a1-r3", &git).unwrap();
assert!(line.starts_with("accepted a1-r3: config/default"), "{line}");
assert_eq!(
head(&ws, &config_ref("default")),
Some(staged),
"the lineage stands at the proposal"
);
assert_eq!(
head(&ws, &proposal_ref("a1-r3")),
None,
"the branch is gone"
);
}
#[test]
fn accept_on_a_stale_proposal_refuses_naming_the_tip_and_moves_nothing() {
let (_h, ws, _parent) = workspace_with_a_proposal("a1-r4");
fixture::amend_config(&ws, &[("souls/worker.md", "a new soul")]);
let tip = head(&ws, &config_ref("default")).unwrap();
let err = accept(&ws, "a1-r4", &RealGit::new()).unwrap_err();
let rendered = err.to_string();
assert!(matches!(err, Error::Stale { .. }), "{rendered}");
assert!(rendered.contains(&short(&tip)), "{rendered}");
assert_eq!(head(&ws, &config_ref("default")), Some(tip), "unmoved");
assert!(
head(&ws, &proposal_ref("a1-r4")).is_some(),
"and unrejected"
);
}
#[test]
fn accept_declines_a_parent_two_lineages_stand_on() {
let (_h, ws, parent) = workspace_with_a_proposal("a1-r5");
RealGit::new()
.run(&repo_git(&ws), &["branch", &config_ref("variant"), &parent])
.unwrap();
let err = accept(&ws, "a1-r5", &RealGit::new()).unwrap_err();
assert!(matches!(err, Error::Ambiguous { .. }), "{err}");
assert!(err.to_string().contains("variant"), "{err}");
assert_eq!(head(&ws, &config_ref("default")), Some(parent), "unmoved");
}
#[test]
fn reject_deletes_only_the_proposal() {
let (_h, ws, parent) = workspace_with_a_proposal("a1-r6");
let line = reject(&ws, "a1-r6", &RealGit::new()).unwrap();
assert!(line.starts_with("rejected a1-r6"), "{line}");
assert_eq!(head(&ws, &proposal_ref("a1-r6")), None);
assert_eq!(head(&ws, &config_ref("default")), Some(parent));
}
#[test]
fn show_renders_the_message_and_the_whole_diff() {
let (_h, ws, _parent) = workspace_with_a_proposal("a1-r7");
let out = show(&ws, "a1-r7", &RealGit::new()).unwrap();
assert!(out.contains("notes: record the lesson"), "{out}");
assert!(out.contains("+the lesson"), "{out}");
}
#[test]
fn an_id_naming_no_proposal_is_declined_with_the_ones_there_are() {
let (_h, ws, _parent) = workspace_with_a_proposal("a1-r8");
let err = show(&ws, "a1-r9", &RealGit::new()).unwrap_err();
assert!(matches!(err, Error::Unknown { .. }), "{err}");
assert!(err.to_string().contains("a1-r8"), "{err}");
}
#[test]
fn a_workspace_with_no_proposals_lists_none() {
let (_h, ws) = fixture::workspace();
assert!(list(&ws, &RealGit::new()).unwrap().is_empty());
}
#[test]
fn a_path_that_is_no_workspace_is_declined_before_any_ref_is_read() {
let dir = TempDir::new().unwrap();
let err = list(dir.path(), &RealGit::new()).unwrap_err();
assert!(matches!(err, Error::Layout(_)), "{err}");
}
struct NoGit;
impl GitRunner for NoGit {
fn run(&self, _d: &Path, _a: &[&str]) -> std::io::Result<()> {
Err(std::io::Error::other("git boom"))
}
fn run_capture(&self, _d: &Path, _a: &[&str]) -> std::io::Result<String> {
Err(std::io::Error::other("git boom"))
}
}
#[test]
fn an_unreadable_proposal_namespace_is_a_git_error_not_an_empty_listing() {
let (_h, ws) = fixture::workspace();
let err = list(&ws, &NoGit).unwrap_err();
assert!(
matches!(
err,
Error::Git {
op: "for-each-ref proposal/",
..
}
),
"{err:?}"
);
}
#[test]
fn an_unreadable_config_namespace_surfaces_from_the_freshness_query() {
let (_h, ws, parent) = workspace_with_a_proposal("a1-r9");
let err = heads_at(&ws, &parent, &NoGit).unwrap_err();
assert!(
matches!(
err,
Error::Git {
op: "for-each-ref config/",
..
}
),
"{err:?}"
);
}