use chrono::{Duration, Utc};
use xbp_analytics::{CoverageAvailability, FlowProjection, TimeWindow};
use xbp_delivery::{DeliveryEvent, DeliveryEventKind, SourceSystem, WorkItemId};
fn event(
id: &str,
kind: DeliveryEventKind,
work_item: &str,
occurred_at: chrono::DateTime<Utc>,
) -> DeliveryEvent {
let mut event = DeliveryEvent::new(id, kind, SourceSystem::Xbp, id);
event.work_item_id = Some(WorkItemId(work_item.into()));
event.occurred_at = occurred_at;
event
}
#[test]
fn flow_completion_cohort_reconstructs_lifecycle_before_the_window() {
let now = Utc::now();
let events = vec![
event(
"created",
DeliveryEventKind::WorkItemCreated,
"item",
now - Duration::days(2),
),
event(
"started",
DeliveryEventKind::WorkItemStarted,
"item",
now - Duration::days(1),
),
event(
"completed",
DeliveryEventKind::WorkItemCompleted,
"item",
now,
),
];
let projection =
FlowProjection::from_events(&events, TimeWindow::new(now - Duration::hours(12), now))
.expect("flow projection");
assert_eq!(projection.completed_count, 1);
assert_eq!(projection.coverage.eligible, 1);
assert_eq!(projection.coverage.observed, 1);
}
#[test]
fn flow_coverage_keeps_completed_work_with_missing_lifecycle_evidence() {
let now = Utc::now();
let events = vec![event(
"completed",
DeliveryEventKind::WorkItemCompleted,
"item",
now,
)];
let projection =
FlowProjection::from_events(&events, TimeWindow::new(now - Duration::hours(12), now))
.expect("flow projection");
assert_eq!(projection.coverage.eligible, 1);
assert_eq!(projection.coverage.observed, 0);
assert_eq!(projection.coverage.sufficient, 0);
assert_eq!(
projection.coverage.availability,
CoverageAvailability::Unavailable
);
}
#[test]
fn flow_reconstructs_multiple_block_intervals_and_current_blocked_wip() {
let now = Utc::now();
let events = vec![
event(
"created",
DeliveryEventKind::WorkItemCreated,
"done",
now - Duration::days(2),
),
event(
"started",
DeliveryEventKind::WorkItemStarted,
"done",
now - Duration::days(1),
),
event(
"blocked-1",
DeliveryEventKind::WorkItemBlocked,
"done",
now - Duration::hours(20),
),
event(
"unblocked-1",
DeliveryEventKind::WorkItemUnblocked,
"done",
now - Duration::hours(18),
),
event(
"blocked-2",
DeliveryEventKind::WorkItemBlocked,
"done",
now - Duration::hours(10),
),
event(
"unblocked-2",
DeliveryEventKind::WorkItemUnblocked,
"done",
now - Duration::hours(9),
),
event(
"completed",
DeliveryEventKind::WorkItemCompleted,
"done",
now,
),
event(
"wip-created",
DeliveryEventKind::WorkItemCreated,
"wip",
now - Duration::hours(4),
),
event(
"wip-blocked",
DeliveryEventKind::WorkItemBlocked,
"wip",
now - Duration::hours(2),
),
];
let projection =
FlowProjection::from_events(&events, TimeWindow::new(now - Duration::days(3), now))
.expect("flow projection");
assert_eq!(projection.blocked_wip, 1);
assert_eq!(projection.blocked_time_hours.mean, Some(3.0));
}
#[test]
fn flow_rejects_invalid_block_and_lifecycle_transitions() {
let now = Utc::now();
let unmatched_unblock = vec![event(
"unblocked",
DeliveryEventKind::WorkItemUnblocked,
"item",
now,
)];
assert!(FlowProjection::from_events(
&unmatched_unblock,
TimeWindow::new(now - Duration::hours(1), now),
)
.is_err());
let completion_before_start = vec![
event(
"created",
DeliveryEventKind::WorkItemCreated,
"item",
now - Duration::hours(2),
),
event("started", DeliveryEventKind::WorkItemStarted, "item", now),
event(
"completed",
DeliveryEventKind::WorkItemCompleted,
"item",
now - Duration::hours(1),
),
];
assert!(FlowProjection::from_events(
&completion_before_start,
TimeWindow::new(now - Duration::days(1), now),
)
.is_err());
}
#[test]
fn flow_v1_projection_preserves_legacy_metric_names() {
let now = Utc::now();
let events = vec![
event(
"created",
DeliveryEventKind::WorkItemCreated,
"item",
now - Duration::hours(2),
),
event(
"started",
DeliveryEventKind::WorkItemStarted,
"item",
now - Duration::hours(1),
),
event(
"completed",
DeliveryEventKind::WorkItemCompleted,
"item",
now,
),
];
let projection =
FlowProjection::from_events(&events, TimeWindow::new(now - Duration::days(1), now))
.expect("flow projection");
let legacy = projection.to_v1();
assert_eq!(legacy.wip, 0);
assert!(legacy.lead_time.is_some());
}