use super::{World, ball, fake_bl, fake_fail, fake_lernie};
use crate::binding::{work_worktree_path, workspace_path};
use crate::cli_outbound::Cli;
use crate::names::MintError;
use crate::opslog::{SYNTHETIC_EXIT, YOG_STEP};
use crate::projects::join::JoinState;
use crate::start::{
Payload, StartError, Target, cross_check_claim, execute_claim, execute_create,
execute_ensure_workspace, on_mint, resolve_name, resolve_worktree,
};
use crate::test_support::spawn_guard;
use std::path::PathBuf;
#[test]
fn create_stamps_the_workspace_name_and_captures_the_id() {
let _g = spawn_guard();
let w = World::new();
let bl = Cli::new(fake_bl(w.bin.path(), "bl-new9", w.balls.path()));
let id = execute_create(
&bl,
w.state.path(),
"TS",
w.project.path(),
"Wire",
"the body",
"cobalt-gecko",
)
.unwrap();
assert_eq!(id, "bl-new9");
assert_eq!(
&w.ops()[0].argv[1..],
&[
"create",
"Wire",
"--as",
"cobalt-gecko",
"--body",
"the body"
]
);
}
#[test]
fn create_elides_an_empty_body() {
let _g = spawn_guard();
let w = World::new();
let bl = Cli::new(fake_bl(w.bin.path(), "bl-1", w.balls.path()));
execute_create(&bl, w.state.path(), "TS", w.project.path(), "T", "", "n").unwrap();
assert_eq!(&w.ops()[0].argv[1..], &["create", "T", "--as", "n"]);
}
#[test]
fn create_errors_on_a_nonzero_exit() {
let _g = spawn_guard();
let w = World::new();
let bl = Cli::new(fake_fail(w.bin.path(), "bl", "boom"));
let err =
execute_create(&bl, w.state.path(), "TS", w.project.path(), "T", "", "n").unwrap_err();
assert!(matches!(err, StartError::VerbFailed { verb: "create", .. }));
}
#[test]
fn claim_stamps_the_name_and_cross_checks_the_canonical_worktree() {
let _g = spawn_guard();
let w = World::new();
let canonical = work_worktree_path(w.balls.path(), w.project.path(), "bl-7", None);
let bl = Cli::new(fake_bl(w.bin.path(), "x", &canonical));
let r = execute_claim(
&bl,
w.state.path(),
"TS",
w.project.path(),
"bl-7",
"cobalt-gecko",
w.balls.path(),
)
.unwrap();
assert_eq!(r.worktree, canonical);
assert!(!r.suffixed);
assert_eq!(
&w.ops()[0].argv[1..],
&["claim", "bl-7", "--as", "cobalt-gecko"]
);
}
#[test]
fn claim_errors_on_a_nonzero_exit() {
let _g = spawn_guard();
let w = World::new();
let bl = Cli::new(fake_fail(w.bin.path(), "bl", "blocked"));
let err = execute_claim(
&bl,
w.state.path(),
"TS",
w.project.path(),
"bl-7",
"n",
w.balls.path(),
)
.unwrap_err();
let StartError::VerbFailed { verb, outcome } = err else {
panic!("expected VerbFailed, got {err:?}");
};
assert_eq!(verb, "claim");
assert_eq!(outcome.stderr, "blocked\n");
}
#[test]
fn cross_check_accepts_the_claimant_suffix_variant() {
let w = World::new();
let suffixed = work_worktree_path(w.balls.path(), w.project.path(), "bl-9", Some("n"));
let r = cross_check_claim(
&suffixed.display().to_string(),
w.balls.path(),
w.project.path(),
"bl-9",
"n",
w.state.path(),
"TS",
)
.unwrap();
assert!(r.suffixed);
assert_eq!(r.worktree, suffixed);
assert!(w.ops().is_empty(), "a matched claim logs no step-failure");
}
#[test]
fn cross_check_drift_is_surfaced_loudly_and_logged() {
let w = World::new();
let err = cross_check_claim(
"/somewhere/unexpected",
w.balls.path(),
w.project.path(),
"bl-9",
"n",
w.state.path(),
"TS",
)
.unwrap_err();
assert!(matches!(err, StartError::Drift { .. }));
let e = &w.ops()[0];
assert_eq!(e.argv, [YOG_STEP, "cross-check"]);
assert_eq!(e.exit, SYNTHETIC_EXIT);
assert!(e.stderr.contains("drift"));
}
#[test]
fn ensure_skips_when_the_workspace_already_exists() {
let w = World::new();
let ws = workspace_path(w.yog.path(), "n");
std::fs::create_dir_all(ws.join("repo.git")).unwrap();
let lernie = Cli::new("/definitely/not/a/real/lernie");
let created = execute_ensure_workspace(&lernie, w.state.path(), "TS", &ws).unwrap();
assert!(!created, "existing workspace skipped");
assert!(w.ops().is_empty(), "skip runs and logs nothing");
}
#[test]
fn ensure_creates_the_workspace_and_logs() {
let _g = spawn_guard();
let w = World::new();
let ws = workspace_path(w.yog.path(), "cobalt-gecko");
let lernie = Cli::new(fake_lernie(w.bin.path()));
assert!(execute_ensure_workspace(&lernie, w.state.path(), "TS", &ws).unwrap());
assert!(ws.parent().unwrap().is_dir(), "parent chain mkdir -p'd");
assert_eq!(
&w.ops()[0].argv[1..],
&["new", ws.to_string_lossy().as_ref()]
);
}
#[test]
fn ensure_errors_and_logs_on_a_nonzero_new() {
let _g = spawn_guard();
let w = World::new();
let ws = workspace_path(w.yog.path(), "n");
let lernie = Cli::new(fake_fail(w.bin.path(), "lernie", "disk full"));
let err = execute_ensure_workspace(&lernie, w.state.path(), "TS", &ws).unwrap_err();
assert!(matches!(err, StartError::VerbFailed { verb: "new", .. }));
}
#[test]
fn ensure_logs_a_mkdir_step_failure() {
let w = World::new();
let blocker = w.yog.path().join("blocked");
std::fs::write(&blocker, b"x").unwrap();
let ws = blocker.join("workspaces").join("n");
let lernie = Cli::new("/definitely/not/a/real/lernie");
let err = execute_ensure_workspace(&lernie, w.state.path(), "TS", &ws).unwrap_err();
assert!(matches!(err, StartError::Io(_)));
assert_eq!(w.ops()[0].argv, [YOG_STEP, "mkdir"]);
}
#[test]
fn on_mint_passes_a_name_through() {
let w = World::new();
let name = on_mint(
Ok("cobalt-gecko".to_owned()),
w.state.path(),
"TS",
w.home.path(),
)
.unwrap();
assert_eq!(name, "cobalt-gecko");
assert!(w.ops().is_empty(), "a clean mint logs nothing");
}
#[test]
fn on_mint_logs_an_exhausted_pool() {
let w = World::new();
let err = on_mint(
Err(MintError::Exhausted(6)),
w.state.path(),
"TS",
w.home.path(),
)
.unwrap_err();
assert!(matches!(err, StartError::Mint(MintError::Exhausted(6))));
let e = &w.ops()[0];
assert_eq!(e.argv, [YOG_STEP, "mint"]);
assert_eq!(e.exit, SYNTHETIC_EXIT);
assert!(e.stderr.contains("pool exhausted"));
}
#[test]
fn resolve_name_returns_existing_and_mints() {
let w = World::new();
let existing = w.inputs(
Target::Existing {
workspace: workspace_path(w.yog.path(), "alpha"),
},
Payload::Bare,
);
assert_eq!(
resolve_name(&existing, &mut super::rng(), w.state.path(), "TS").unwrap(),
"alpha"
);
let mint = w.inputs(Target::Mint, Payload::Bare);
let name = resolve_name(&mint, &mut super::rng(), w.state.path(), "TS").unwrap();
assert!(name.contains('-'));
assert!(name.bytes().all(|b| b.is_ascii_lowercase() || b == b'-'));
}
#[test]
fn resolve_worktree_prefers_the_claim_then_disk_then_canonical() {
let w = World::new();
let (balls, project, name) = (w.balls.path(), w.project.path(), "cobalt-gecko");
let existing = |id: &str| ball(project, id, JoinState::Bound);
assert_eq!(resolve_worktree(&Payload::Bare, balls, name, None), None);
let claimed = PathBuf::from("/claimed/wt-suffixed");
assert_eq!(
resolve_worktree(&existing("bl-1"), balls, name, Some(claimed.clone())),
Some(claimed),
);
assert_eq!(
resolve_worktree(&existing("bl-2"), balls, name, None),
Some(work_worktree_path(balls, project, "bl-2", None)),
);
let suffixed = work_worktree_path(balls, project, "bl-3", Some(name));
std::fs::create_dir_all(&suffixed).unwrap();
assert_eq!(
resolve_worktree(&existing("bl-3"), balls, name, None),
Some(suffixed),
);
}