use super::{World, ball, fake_bl, fake_fail, rng};
use crate::binding::{work_worktree_path, workspace_path};
use crate::cli_outbound::{Binary, Cli};
use crate::projects::join::JoinState;
use crate::start::{BallSpec, Deps, Payload, StartError, Target, prepare};
use crate::test_support::{OkProbe, spawn_guard};
use crate::world::toolgate::{self, ToolProbe, ToolState};
use std::path::Path;
fn deps(w: &World, bl: &Cli, lernie: &Cli) -> Deps {
Deps {
bl: bl.clone(),
lernie: lernie.clone(),
state_root: w.state.path().to_path_buf(),
gate: toolgate::probe(&OkProbe),
}
}
struct Fail(Binary, &'static str);
impl ToolProbe for Fail {
fn status(&self, binary: Binary, args: &[&str]) -> std::io::Result<i32> {
Ok(i32::from(binary == self.0 && args.first() == Some(&self.1)) * 2)
}
}
#[test]
fn prepare_refuses_a_bare_start_on_a_stale_lernie_before_any_spawn() {
let w = World::new();
let deps = Deps {
bl: Cli::new("/no/bl"),
lernie: Cli::new("/no/lernie"),
state_root: w.state.path().to_path_buf(),
gate: toolgate::probe(&Fail(Binary::Lernie, "prime")),
};
let inputs = w.inputs(Target::Mint, Payload::Bare);
let err = prepare(&deps, &inputs, &mut rng(), "TS").unwrap_err();
let StartError::GateRefused(refusal) = err else {
panic!("expected GateRefused, got {err:?}");
};
assert_eq!(refusal.tool, "lernie");
assert!(matches!(&refusal.state, ToolState::Mismatch { verb, .. } if verb == "prime"));
assert_eq!(
w.verbs(),
vec!["gate"],
"no lernie/bl spawn — just the gate row"
);
}
#[test]
fn prepare_ball_rung_refuses_on_a_bad_bl_after_lernie_passes() {
let w = World::new();
let deps = Deps {
bl: Cli::new("/no/bl"),
lernie: Cli::new("/no/lernie"),
state_root: w.state.path().to_path_buf(),
gate: toolgate::probe(&Fail(Binary::Bl, "create")),
};
let inputs = w.inputs(
Target::Existing {
workspace: workspace_path(w.yog.path(), "cobalt-gecko"),
},
ball(w.project.path(), "bl-r", JoinState::ReadyStartable),
);
let err = prepare(&deps, &inputs, &mut rng(), "TS").unwrap_err();
let StartError::GateRefused(refusal) = err else {
panic!("expected GateRefused, got {err:?}");
};
assert_eq!(refusal.tool, "bl");
assert!(matches!(&refusal.state, ToolState::Mismatch { verb, .. } if verb == "create"));
assert_eq!(w.verbs(), vec!["gate"]);
}
fn mark_seeded(w: &World) {
let lernie = crate::world::layout_under(w.yog.path()).lernie;
std::fs::create_dir_all(&lernie).unwrap();
std::fs::write(lernie.join("models.yaml"), b"models: {}\n").unwrap();
}
#[test]
fn prepare_bare_bootstrap_mints_seeds_and_news() {
let _g = spawn_guard();
let w = World::new();
let lernie = w.lernie();
let bl = Cli::new("/no/bl"); let inputs = w.inputs(Target::Mint, Payload::Bare);
let p = prepare(&deps(&w, &bl, &lernie), &inputs, &mut rng(), "TS").unwrap();
assert!(p.name.contains('-'), "a fresh name minted");
assert_eq!(p.cwd, w.home.path(), "bare cwd is ~");
assert_eq!(p.goal, "", "the operator types the bare payload");
assert_eq!(p.workspace, workspace_path(w.yog.path(), &p.name));
assert_eq!(w.verbs(), vec!["prime", "new"]);
assert_eq!(
w.ops()[1].argv[2],
p.workspace.to_string_lossy(),
"`lernie new` targets the minted workspace"
);
}
#[test]
fn prepare_prompt_into_existing_skips_prime_new_and_mint() {
let _g = spawn_guard();
let w = World::new();
mark_seeded(&w);
let name = "cobalt-gecko";
std::fs::create_dir_all(workspace_path(w.yog.path(), name).join("repo.git")).unwrap();
let lernie = Cli::new("/no/lernie"); let bl = Cli::new("/no/bl");
let inputs = w.inputs(
Target::Existing {
workspace: workspace_path(w.yog.path(), name),
},
Payload::Bare,
);
let p = prepare(&deps(&w, &bl, &lernie), &inputs, &mut rng(), "TS").unwrap();
assert_eq!(p.name, name);
assert!(
w.ops().is_empty(),
"seeded + existing → nothing spawns (S1-T1)"
);
}
#[test]
fn prepare_path_rung_composes_the_target_and_runs_no_bl() {
let _g = spawn_guard();
let w = World::new();
let lernie = w.lernie();
let bl = Cli::new("/no/bl");
let dir = w.home.path().join("work");
let inputs = w.inputs(
Target::Existing {
workspace: workspace_path(w.yog.path(), "cobalt-gecko"),
},
Payload::Path { dir: dir.clone() },
);
let p = prepare(&deps(&w, &bl, &lernie), &inputs, &mut rng(), "TS").unwrap();
assert_eq!(p.cwd, dir, "driver cwd is the directory");
assert!(p.goal.contains(&dir.display().to_string()));
assert!(!w.verbs().iter().any(|v| v == "claim" || v == "create"));
}
#[test]
fn prepare_ball_ready_claims_after_new() {
let _g = spawn_guard();
let w = World::new();
let canonical = work_worktree_path(w.balls.path(), w.project.path(), "bl-r", None);
let bl = Cli::new(fake_bl(w.bin.path(), "x", &canonical));
let lernie = w.lernie();
let inputs = w.inputs(
Target::Existing {
workspace: workspace_path(w.yog.path(), "cobalt-gecko"),
},
ball(w.project.path(), "bl-r", JoinState::ReadyStartable),
);
let p = prepare(&deps(&w, &bl, &lernie), &inputs, &mut rng(), "TS").unwrap();
assert_eq!(p.cwd, canonical, "ball cwd is the worktree");
assert!(p.goal.contains("Ball bl-r: T"));
assert_eq!(w.verbs(), vec!["prime", "new", "claim"]);
assert_eq!(
&w.ops()[2].argv[1..],
&["claim", "bl-r", "--as", "cobalt-gecko"],
"claim stamped with the workspace name"
);
}
#[test]
fn prepare_new_ball_creates_then_converges_to_one_claim() {
let _g = spawn_guard();
let w = World::new();
let canonical = work_worktree_path(w.balls.path(), w.project.path(), "bl-mint", None);
let bl = Cli::new(fake_bl(w.bin.path(), "bl-mint", &canonical));
let lernie = w.lernie();
let payload = Payload::Ball {
project: w.project.path().to_path_buf(),
ball: BallSpec::New {
title: "Fresh".to_owned(),
body: "New body".to_owned(),
},
};
let inputs = w.inputs(
Target::Existing {
workspace: workspace_path(w.yog.path(), "cobalt-gecko"),
},
payload,
);
let p = prepare(&deps(&w, &bl, &lernie), &inputs, &mut rng(), "TS").unwrap();
assert!(
p.goal.contains("Ball bl-mint: Fresh"),
"re-planned as existing"
);
assert_eq!(w.verbs(), vec!["prime", "new", "create", "claim"]);
}
#[test]
fn prepare_bound_ball_resumes_without_a_claim_or_mint() {
let _g = spawn_guard();
let w = World::new();
mark_seeded(&w);
std::fs::create_dir_all(workspace_path(w.yog.path(), "cobalt-gecko").join("repo.git")).unwrap();
let bl = Cli::new(fake_fail(w.bin.path(), "bl", "should not run"));
let lernie = Cli::new("/no/lernie");
let inputs = w.inputs(
Target::Existing {
workspace: workspace_path(w.yog.path(), "cobalt-gecko"),
},
ball(w.project.path(), "bl-c", JoinState::Bound),
);
let p = prepare(&deps(&w, &bl, &lernie), &inputs, &mut rng(), "TS").unwrap();
assert_eq!(
p.cwd,
work_worktree_path(w.balls.path(), w.project.path(), "bl-c", None)
);
assert!(w.ops().is_empty(), "resume: no claim, no mint, no re-seed");
}
#[test]
fn prepare_ball_aborts_before_any_bl_on_a_substrate_failure() {
let _g = spawn_guard();
let w = World::new();
let bl = Cli::new(fake_bl(w.bin.path(), "x", Path::new("/wt")));
let lernie = Cli::new(fake_fail(w.bin.path(), "lernie", "no seed"));
let inputs = w.inputs(
Target::Existing {
workspace: workspace_path(w.yog.path(), "cobalt-gecko"),
},
ball(w.project.path(), "bl-r", JoinState::ReadyStartable),
);
assert!(prepare(&deps(&w, &bl, &lernie), &inputs, &mut rng(), "TS").is_err());
assert_eq!(
w.verbs(),
vec!["prime"],
"aborted at the seed — no bl mutation"
);
}