use crate::binding::{work_worktree_path, workspace_path};
use crate::projects::join::JoinState;
use crate::start::{
BallSpec, Payload, PlanInputs, Step, is_resume_eligible, is_start_eligible, plan,
};
use std::path::{Path, PathBuf};
const NAME: &str = "cobalt-gecko";
const YOG: &str = "/yog";
const BALLS: &str = "/balls";
const HOME: &str = "/home/op";
const PROJ: &str = "/proj";
fn inputs(payload: Payload) -> PlanInputs {
PlanInputs {
name: NAME.to_owned(),
workspace: ws(),
payload,
home: PathBuf::from(HOME),
yog_data_root: PathBuf::from(YOG),
balls_state_root: PathBuf::from(BALLS),
}
}
fn ws() -> PathBuf {
workspace_path(Path::new(YOG), NAME)
}
fn ball_payload(join: JoinState) -> Payload {
Payload::Ball {
project: PathBuf::from(PROJ),
ball: BallSpec::Existing {
id: "bl-1".to_owned(),
title: "T".to_owned(),
body: "B".to_owned(),
join,
},
}
}
#[test]
fn bare_plans_seed_new_prompt() {
assert_eq!(
plan(&inputs(Payload::Bare)),
vec![
Step::EnsureSeeded,
Step::EnsureWorkspace { workspace: ws() },
Step::Prompt {
name: NAME.to_owned(),
workspace: ws(),
cwd: PathBuf::from(HOME),
goal: String::new(),
},
]
);
}
#[test]
fn path_rung_targets_the_dir_verbatim() {
let dir = PathBuf::from("/work/here");
let steps = plan(&inputs(Payload::Path { dir: dir.clone() }));
assert!(matches!(steps[0], Step::EnsureSeeded));
assert!(matches!(steps[1], Step::EnsureWorkspace { .. }));
let Some(Step::Prompt {
cwd, goal, name, ..
}) = steps.last()
else {
panic!("path plan ends in a prompt");
};
assert_eq!(*cwd, dir, "driver cwd is the directory");
assert_eq!(name, NAME);
assert!(goal.contains("/work/here"), "target preamble names the dir");
assert!(
!goal.contains("You are"),
"identity is stamped at fire, not here"
);
}
#[test]
fn ball_ready_claims_after_new_and_binds_the_worktree() {
let steps = plan(&inputs(ball_payload(JoinState::ReadyStartable)));
assert!(matches!(steps[0], Step::EnsureSeeded));
assert!(matches!(steps[1], Step::EnsureWorkspace { .. }));
assert_eq!(
steps[2],
Step::Claim {
project: PathBuf::from(PROJ),
id: "bl-1".to_owned(),
name: NAME.to_owned(),
},
"claim stamped with the workspace name, after the substrate"
);
let wt = work_worktree_path(Path::new(BALLS), Path::new(PROJ), "bl-1", None);
let Step::Prompt { cwd, goal, .. } = &steps[3] else {
panic!("ends in a prompt");
};
assert_eq!(*cwd, wt, "driver cwd is the work worktree");
assert!(goal.contains("Ball bl-1: T"));
assert!(
goal.contains(&wt.display().to_string()),
"worktree preamble"
);
}
#[test]
fn ball_bound_drops_the_claim() {
let steps = plan(&inputs(ball_payload(JoinState::Bound)));
assert!(!steps.iter().any(|s| matches!(s, Step::Claim { .. })));
assert!(steps.iter().any(|s| matches!(s, Step::Prompt { .. })));
assert_eq!(steps.len(), 3, "seed, new, prompt");
}
#[test]
fn ball_new_defers_to_a_single_create_after_the_substrate() {
let payload = Payload::Ball {
project: PathBuf::from(PROJ),
ball: BallSpec::New {
title: "Fresh".to_owned(),
body: "do".to_owned(),
},
};
assert_eq!(
plan(&inputs(payload)),
vec![
Step::EnsureSeeded,
Step::EnsureWorkspace { workspace: ws() },
Step::Create {
project: PathBuf::from(PROJ),
title: "Fresh".to_owned(),
body: "do".to_owned(),
},
],
"create is deferred to after seed+new; the id re-plans the rest"
);
}
#[test]
fn eligibility_is_the_ready_ball() {
assert!(is_start_eligible(JoinState::ReadyStartable));
assert!(!is_start_eligible(JoinState::Bound));
assert!(!is_start_eligible(JoinState::Blocked));
assert!(!is_start_eligible(JoinState::ClaimedElsewhere));
assert!(!is_start_eligible(JoinState::Delivered));
assert!(!is_start_eligible(JoinState::UnassignedWorkspace));
assert!(!is_start_eligible(JoinState::OrphanedProject));
}
#[test]
fn resume_eligibility_is_the_bound_ball() {
assert!(is_resume_eligible(JoinState::Bound));
assert!(!is_resume_eligible(JoinState::ReadyStartable));
assert!(!is_resume_eligible(JoinState::Blocked));
assert!(!is_resume_eligible(JoinState::ClaimedElsewhere));
assert!(!is_resume_eligible(JoinState::Delivered));
assert!(!is_resume_eligible(JoinState::UnassignedWorkspace));
assert!(!is_resume_eligible(JoinState::OrphanedProject));
}