use crate::session::Session;
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
Working,
Stopped,
Other,
}
impl Phase {
pub fn of(state: crate::session::ActivityState) -> Phase {
use crate::session::ActivityState;
match state {
ActivityState::Working => Phase::Working,
ActivityState::WaitingForInput => Phase::Stopped,
ActivityState::Asking | ActivityState::ApiError => Phase::Other,
}
}
}
#[derive(Debug, Default)]
pub struct Seen {
working: HashSet<String>,
done: HashSet<String>,
live: Vec<String>,
}
impl Seen {
pub fn observe<'a>(
&mut self,
live: impl IntoIterator<Item = (&'a Session, Phase)>,
viewed: Option<&str>,
) -> bool {
let before = self.done.len();
let mut changed = false;
let live: Vec<(&Session, Phase)> = live.into_iter().collect();
self.live.clear();
self.live.reserve(live.len());
for (session, _) in &live {
let next = self.live.len();
self.live.push(String::new());
session.key_into(&mut self.live[next]);
}
let present: HashSet<&str> = self.live.iter().map(String::as_str).collect();
for ((_, phase), key) in live.iter().zip(&self.live) {
let key = key.as_str();
let looking = viewed == Some(key);
match *phase {
Phase::Working => {
if !self.working.contains(key) {
self.working.insert(key.to_string());
}
changed |= self.done.remove(key);
}
Phase::Stopped => {
if self.working.remove(key) && !looking {
changed |= self.done.insert(key.to_string());
}
}
Phase::Other => {}
}
if looking {
changed |= self.done.remove(key);
}
}
self.working.retain(|key| present.contains(key.as_str()));
self.done.retain(|key| present.contains(key.as_str()));
changed || self.done.len() != before
}
pub fn is_done(&self, key: &str) -> bool {
self.done.contains(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn row(id: &str) -> Session {
Session::new(crate::pricing::Provider::Claude, id.into())
}
#[test]
fn a_turn_that_ends_unwatched_is_marked_until_looked_at() {
let mut seen = Seen::default();
let a = row("a");
let key = a.key();
assert!(!seen.observe([(&a, Phase::Working)], None));
assert!(!seen.is_done(&key), "working is not done");
assert!(seen.observe([(&a, Phase::Stopped)], None), "no frame owed");
assert!(seen.is_done(&key), "an unwatched stop was not marked");
assert!(!seen.observe([(&a, Phase::Stopped)], None));
assert!(seen.is_done(&key));
assert!(seen.observe([(&a, Phase::Stopped)], Some(key.as_str())));
assert!(!seen.is_done(&key), "looking did not clear it");
seen.observe([(&a, Phase::Stopped)], None);
assert!(!seen.is_done(&key), "the same stop was marked twice");
}
#[test]
fn a_stop_nobody_saw_begin_is_not_marked() {
let mut seen = Seen::default();
let a = row("a");
seen.observe([(&a, Phase::Stopped)], None);
assert!(!seen.is_done(&a.key()));
}
#[test]
fn a_turn_that_ends_while_watched_is_not_marked() {
let mut seen = Seen::default();
let a = row("a");
let key = a.key();
seen.observe([(&a, Phase::Working)], Some(key.as_str()));
seen.observe([(&a, Phase::Stopped)], Some(key.as_str()));
seen.observe([(&a, Phase::Stopped)], None);
assert!(!seen.is_done(&a.key()));
}
#[test]
fn a_new_turn_clears_the_mark_until_it_ends() {
let mut seen = Seen::default();
let a = row("a");
seen.observe([(&a, Phase::Working)], None);
seen.observe([(&a, Phase::Stopped)], None);
assert!(seen.observe([(&a, Phase::Working)], None));
assert!(!seen.is_done(&a.key()));
seen.observe([(&a, Phase::Stopped)], None);
assert!(seen.is_done(&a.key()));
}
#[test]
fn a_question_mid_turn_leaves_the_turn_armed() {
let mut seen = Seen::default();
let a = row("a");
seen.observe([(&a, Phase::Working)], None);
seen.observe([(&a, Phase::Other)], None);
assert!(!seen.is_done(&a.key()), "a question is not a finished turn");
seen.observe([(&a, Phase::Stopped)], None);
assert!(seen.is_done(&a.key()));
}
#[test]
fn an_ended_session_is_forgotten() {
let mut seen = Seen::default();
let a = row("a");
let b = row("b");
seen.observe([(&a, Phase::Working), (&b, Phase::Working)], None);
seen.observe([(&a, Phase::Stopped), (&b, Phase::Working)], None);
assert!(seen.is_done(&a.key()));
assert!(seen.observe([(&b, Phase::Working)], None), "no frame owed");
assert!(!seen.is_done(&a.key()));
seen.observe([(&a, Phase::Stopped)], None);
assert!(!seen.is_done(&a.key()));
}
#[test]
fn a_row_reads_its_phase_off_its_dot() {
use crate::session::ActivityState;
assert_eq!(Phase::of(ActivityState::Working), Phase::Working);
assert_eq!(Phase::of(ActivityState::WaitingForInput), Phase::Stopped);
assert_eq!(Phase::of(ActivityState::Asking), Phase::Other);
assert_eq!(Phase::of(ActivityState::ApiError), Phase::Other);
}
}