use leviath_core::run_archive::RunPoint;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct StageVisit {
pub(super) stage: String,
pub(super) entered_at: i64,
pub(super) left_at: Option<i64>,
pub(super) iterations: usize,
pub(super) first_point: usize,
}
#[derive(Debug, Clone, Default)]
pub(super) struct RunHistoryCache {
pub(super) run_id: String,
pub(super) points: Vec<RunPoint>,
pub(super) visits: Vec<StageVisit>,
pub(super) loaded_at_tick: u64,
}
pub(super) const HISTORY_TTL_TICKS: u64 = 10;
pub(super) fn derive_visits(points: &[RunPoint]) -> Vec<StageVisit> {
let mut visits: Vec<StageVisit> = Vec::new();
for (idx, point) in points.iter().enumerate() {
let stage = point.meta.current_stage.clone();
match visits.last_mut() {
Some(last) if last.stage == stage => {
last.iterations = last.iterations.max(point.meta.iteration);
}
other => {
if let Some(prev) = other {
prev.left_at = Some(point.at);
}
visits.push(StageVisit {
stage,
entered_at: point.at,
left_at: None,
iterations: point.meta.iteration,
first_point: idx,
});
}
}
}
visits
}
pub(super) fn visit_count(visits: &[StageVisit], stage: &str) -> usize {
visits.iter().filter(|v| v.stage == stage).count()
}
pub(super) fn last_visit<'a>(visits: &'a [StageVisit], stage: &str) -> Option<&'a StageVisit> {
visits.iter().rev().find(|v| v.stage == stage)
}
#[cfg(test)]
mod tests {
use super::*;
use leviath_core::run_meta::{RunMeta, RunStatus};
fn point(stage: &str, iteration: usize, at: i64) -> RunPoint {
let mut meta = RunMeta::new(
"r".to_string(),
"a".to_string(),
"/p".to_string(),
"t".to_string(),
None,
"/w".to_string(),
3,
);
meta.current_stage = stage.to_string();
meta.iteration = iteration;
meta.status = RunStatus::Running;
RunPoint {
meta,
context: leviath_core::run_meta::ContextSnapshot {
stage_name: stage.to_string(),
total_tokens: 0,
max_tokens: 100,
regions: Vec::new(),
},
at,
}
}
#[test]
fn visits_split_on_stage_change_and_count_revisits() {
let points = vec![
point("plan", 1, 10),
point("plan", 2, 20),
point("implement", 1, 30),
point("review", 1, 40),
point("implement", 1, 50), point("implement", 2, 60),
];
let visits = derive_visits(&points);
assert_eq!(visits.len(), 4);
assert_eq!(visits[0].stage, "plan");
assert_eq!(visits[0].entered_at, 10);
assert_eq!(visits[0].left_at, Some(30));
assert_eq!(visits[0].iterations, 2);
assert_eq!(visits[0].first_point, 0);
assert_eq!(visits[3].stage, "implement");
assert_eq!(visits[3].first_point, 4);
assert_eq!(visits[3].left_at, None, "still running");
assert_eq!(visits[3].iterations, 2);
assert_eq!(visit_count(&visits, "implement"), 2);
assert_eq!(visit_count(&visits, "plan"), 1);
assert_eq!(visit_count(&visits, "never"), 0);
assert_eq!(last_visit(&visits, "implement").unwrap().first_point, 4);
assert!(last_visit(&visits, "never").is_none());
}
#[test]
fn an_empty_archive_derives_an_empty_timeline() {
assert!(derive_visits(&[]).is_empty());
}
}