use super::model::{Goal, GoalStatus, SessionLink, SessionTaskState};
use chrono::{TimeZone, Utc};
fn ts() -> chrono::DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 6, 16, 12, 0, 0).unwrap()
}
fn goal_with_links(total: usize, verified: usize) -> Goal {
let mut g = Goal::new("g-test1234", "ship the thing", vec![], ts());
for i in 0..total {
let mut link = SessionLink::launched(format!("s-{i}"), format!("task {i}"));
if i < verified {
link.state = SessionTaskState::Verified;
}
g.sessions.push(link);
}
g.recompute_progress();
g
}
#[test]
fn new_goal_is_pending_zero_progress() {
let g = Goal::new("g-abc", "do work", vec!["passes".into()], ts());
assert_eq!(g.status, GoalStatus::Pending);
assert_eq!(g.progress, 0);
assert!(g.sessions.is_empty());
assert_eq!(g.created, g.updated);
assert_eq!(g.acceptance, vec!["passes".to_string()]);
}
#[test]
fn status_and_state_defaults() {
assert_eq!(GoalStatus::default(), GoalStatus::Pending);
assert_eq!(SessionTaskState::default(), SessionTaskState::Launched);
}
#[test]
fn goal_status_label_matches_variants() {
assert_eq!(GoalStatus::Pending.label(), "Pending");
assert_eq!(GoalStatus::InProgress.label(), "InProgress");
assert_eq!(GoalStatus::Blocked.label(), "Blocked");
assert_eq!(GoalStatus::Done.label(), "Done");
assert_eq!(GoalStatus::Abandoned.label(), "Abandoned");
}
#[test]
fn verified_is_the_only_gate_satisfying_state() {
assert!(SessionTaskState::Verified.is_verified());
assert!(!SessionTaskState::Launched.is_verified());
assert!(!SessionTaskState::Running.is_verified());
assert!(!SessionTaskState::Failed.is_verified());
}
#[test]
fn progress_one_of_three_verified_is_33() {
let g = goal_with_links(3, 1);
assert_eq!(g.progress, 33, "1/3 verified must round to 33%");
}
#[test]
fn progress_all_verified_is_100() {
let g = goal_with_links(3, 3);
assert_eq!(g.progress, 100);
}
#[test]
fn progress_no_links_is_zero() {
let g = goal_with_links(0, 0);
assert_eq!(g.progress, 0);
assert!(!g.all_verified());
}
#[test]
fn progress_two_of_four_is_50() {
let g = goal_with_links(4, 2);
assert_eq!(g.progress, 50);
}
#[test]
fn all_verified_predicate() {
assert!(
!goal_with_links(3, 2).all_verified(),
"any unverified → false"
);
assert!(!goal_with_links(0, 0).all_verified(), "no links → false");
assert!(
goal_with_links(2, 2).all_verified(),
"every link verified → true"
);
}
#[test]
fn launched_link_has_no_evidence() {
let link = SessionLink::launched("s-1", "do x");
assert_eq!(link.state, SessionTaskState::Launched);
assert!(link.evidence.is_none());
}
#[test]
fn goal_serde_roundtrip() {
let mut g = goal_with_links(2, 1);
g.status = GoalStatus::InProgress;
g.sessions[0].evidence = Some("https://example.com/pr/1".into());
g.notes.push("blocked on review".into());
let json = serde_json::to_string(&g).expect("serialise");
let back: Goal = serde_json::from_str(&json).expect("deserialise");
assert_eq!(g, back, "goal must round-trip through JSON unchanged");
}
#[test]
fn enum_wire_forms_are_camel_case() {
assert_eq!(
serde_json::to_string(&GoalStatus::InProgress).unwrap(),
"\"inProgress\""
);
assert_eq!(
serde_json::to_string(&SessionTaskState::Verified).unwrap(),
"\"verified\""
);
}