use super::*;
use chrono::Utc;
use crate::deliverable::{
Deliverable, DeliverableId, DeliverableKind, DeliverableStatus, EstimationTier, Milestone,
MilestoneId, MilestoneStatus,
};
use crate::project::Project;
fn project(name: &str) -> Project {
Project {
name: name.to_string(),
repo_url: format!("https://github.com/acme/{name}"),
default_branch: "main".to_string(),
stack_hint: None,
tags: vec![],
description: None,
gh_user: None,
gh_account: None,
github: None,
commit_name: None,
commit_email: None,
worktree: None,
}
}
fn deliverable(project_name: &str, status: DeliverableStatus) -> Deliverable {
Deliverable {
id: DeliverableId::new(),
project_name: project_name.to_string(),
name: "fixture".to_string(),
description: String::new(),
kind: DeliverableKind::Feature,
ticket_ref: None,
spec_ref: None,
status,
estimated_effort: EstimationTier::M,
created_at: Utc::now(),
target_date: None,
}
}
fn milestone(
project_name: &str,
status: MilestoneStatus,
deliverables: Vec<DeliverableId>,
) -> Milestone {
Milestone {
id: MilestoneId::new(),
project_name: project_name.to_string(),
name: "fixture milestone".to_string(),
description: String::new(),
target_date: Utc::now(),
status,
deliverables,
created_at: Utc::now(),
}
}
#[test]
fn aggregate_portfolio_status_sums_across_projects() {
let projects = vec![project("alpha"), project("beta")];
let missing = DeliverableId::new();
let alpha_live = deliverable("alpha", DeliverableStatus::InProgress);
let alpha_live_id = alpha_live.id;
let deliverables = vec![
alpha_live,
deliverable("beta", DeliverableStatus::Complete),
deliverable("beta", DeliverableStatus::Blocked),
];
let milestones = vec![
milestone(
"alpha",
MilestoneStatus::InProgress,
vec![alpha_live_id, missing],
),
milestone("beta", MilestoneStatus::Shipped, vec![]),
];
let rollup = aggregate_portfolio_status(&projects, &[], &deliverables, &milestones);
assert_eq!(rollup.project_count, 2);
assert_eq!(rollup.totals.deliverables.total, 3);
assert_eq!(rollup.totals.deliverables.in_progress, 1);
assert_eq!(rollup.totals.deliverables.complete, 1);
assert_eq!(rollup.totals.deliverables.blocked, 1);
assert_eq!(rollup.totals.milestones.total, 2);
assert_eq!(rollup.totals.milestones.in_progress, 1);
assert_eq!(rollup.totals.milestones.shipped, 1);
assert_eq!(
rollup.totals.milestones.dangling_deliverable_refs, 1,
"alpha's milestone references a missing deliverable id: {rollup:?}"
);
assert_eq!(rollup.totals.sessions.total, 0);
assert_eq!(rollup.totals.last_activity_at, None);
}
#[test]
fn aggregate_portfolio_status_is_deterministic() {
let projects = vec![project("beta"), project("alpha")];
let deliverables = vec![deliverable("alpha", DeliverableStatus::Proposed)];
let first = aggregate_portfolio_status(&projects, &[], &deliverables, &[]);
let second = aggregate_portfolio_status(&projects, &[], &deliverables, &[]);
assert_eq!(first, second, "same inputs must produce identical output");
let names: Vec<&str> = first
.projects
.iter()
.map(|p| p.project_name.as_str())
.collect();
assert_eq!(names, vec!["alpha", "beta"], "projects must be name-sorted");
}
#[test]
fn aggregate_portfolio_status_empty_portfolio() {
let rollup = aggregate_portfolio_status(&[], &[], &[], &[]);
assert_eq!(rollup.project_count, 0);
assert!(rollup.projects.is_empty());
assert_eq!(rollup.totals.sessions.total, 0);
assert_eq!(rollup.totals.deliverables.total, 0);
assert_eq!(rollup.totals.milestones.total, 0);
assert_eq!(rollup.totals.last_activity_at, None);
}