use std::collections::HashMap;
use travelagent_core::model::{CommentTriage, TourCommentMeta, TourState as CoreTourState};
use travelagent_core::risk::ScoredCommit;
use super::GranularityHint;
#[derive(Debug, Default)]
pub struct TourSessionState {
pub plan: Option<CoreTourState>,
pub comment_meta: HashMap<String, TourCommentMeta>,
pub triage: HashMap<String, CommentTriage>,
pub granularity_hint: Option<GranularityHint>,
pub score_cache: HashMap<String, ScoredCommit>,
pub date_range_cache: Option<(String, String, String)>,
}
impl TourSessionState {
pub fn set_granularity_hint(&mut self, hint: GranularityHint) {
self.granularity_hint = Some(hint);
}
pub fn take_granularity_hint(&mut self) -> Option<GranularityHint> {
self.granularity_hint.take()
}
pub fn cached_score(&self, sha: &str) -> Option<&ScoredCommit> {
self.score_cache.get(sha)
}
pub fn cache_score(&mut self, sha: String, scored: ScoredCommit) {
self.score_cache.insert(sha, scored);
}
pub fn invalidate_score_cache(&mut self) {
self.score_cache.clear();
}
pub fn record_comment_meta(&mut self, comment_id: String, meta: TourCommentMeta) {
self.comment_meta.insert(comment_id, meta);
}
pub fn is_tour_comment(&self, comment_id: &str) -> bool {
self.comment_meta.contains_key(comment_id)
}
pub fn set_triage(&mut self, comment_id: String, triage: CommentTriage) {
self.triage.insert(comment_id, triage);
}
pub fn triage_counts(&self) -> (usize, usize, usize) {
use travelagent_core::model::TourTriageVerdict;
let mut live = 0;
let mut obsolete = 0;
let mut moved = 0;
for t in self.triage.values() {
match t.verdict {
TourTriageVerdict::Live => live += 1,
TourTriageVerdict::LikelyObsolete => obsolete += 1,
TourTriageVerdict::Moved => moved += 1,
}
}
(live, obsolete, moved)
}
pub fn end_tour(&mut self) {
self.plan = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
use travelagent_core::model::{NewCommentLocation, TourTriageVerdict};
fn triage_live() -> CommentTriage {
CommentTriage {
verdict: TourTriageVerdict::Live,
reasoning: String::new(),
new_location: None,
}
}
fn triage_moved() -> CommentTriage {
CommentTriage {
verdict: TourTriageVerdict::Moved,
reasoning: String::new(),
new_location: Some(NewCommentLocation {
file: "a".into(),
line: 1,
}),
}
}
#[test]
fn granularity_hint_round_trips_and_is_one_shot() {
let mut s = TourSessionState::default();
assert!(s.take_granularity_hint().is_none());
s.set_granularity_hint(GranularityHint::Finer);
assert_eq!(s.take_granularity_hint(), Some(GranularityHint::Finer));
assert!(
s.take_granularity_hint().is_none(),
"hint must be consumed exactly once"
);
}
#[test]
fn score_cache_round_trip_and_invalidation() {
let mut s = TourSessionState::default();
let scored = ScoredCommit {
sha: "abc".into(),
risk: travelagent_core::risk::RiskScore::MIN,
summary: String::new(),
};
s.cache_score("abc".into(), scored.clone());
assert_eq!(
s.cached_score("abc").map(|c| c.sha.clone()),
Some("abc".into())
);
s.invalidate_score_cache();
assert!(s.cached_score("abc").is_none());
}
#[test]
fn comment_meta_roundtrips_and_triage_counts() {
let mut s = TourSessionState::default();
assert!(!s.is_tour_comment("x"));
s.record_comment_meta(
"x".into(),
TourCommentMeta {
stop_index: 0,
stop_commit_shas: vec!["a".into()],
file: "f".into(),
line: 1,
},
);
assert!(s.is_tour_comment("x"));
s.set_triage("x".into(), triage_live());
s.set_triage("y".into(), triage_moved());
assert_eq!(s.triage_counts(), (1, 0, 1));
}
#[test]
fn end_tour_clears_plan_but_keeps_meta() {
use travelagent_core::model::TourStop;
use travelagent_core::risk::RiskScore;
let mut s = TourSessionState {
plan: Some(CoreTourState::new(vec![TourStop {
commit_ids: vec!["a".into()],
summary: "s".into(),
risk: RiskScore::MIN,
}])),
..Default::default()
};
s.record_comment_meta(
"x".into(),
TourCommentMeta {
stop_index: 0,
stop_commit_shas: vec!["a".into()],
file: "f".into(),
line: 1,
},
);
s.end_tour();
assert!(s.plan.is_none());
assert!(
s.is_tour_comment("x"),
"comment meta must survive end_tour for post-tour triage"
);
}
}