use chrono::{DateTime, Utc};
use uuid::Uuid;
pub struct SteerInstruction {
pub id: String,
pub text: String,
pub created_at: DateTime<Utc>,
pub turns_left: Option<u32>,
}
#[derive(Default)]
pub struct SteerManager {
instructions: Vec<SteerInstruction>,
}
impl SteerManager {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, text: String, turns: Option<u32>) -> String {
let id = Uuid::new_v4().to_string();
self.instructions.push(SteerInstruction {
id: id.clone(),
text,
created_at: Utc::now(),
turns_left: turns,
});
id
}
pub fn remove(&mut self, id: &str) -> bool {
let before = self.instructions.len();
self.instructions
.retain(|i| !i.id.starts_with(id));
self.instructions.len() < before
}
pub fn tick(&mut self) {
for inst in self.instructions.iter_mut() {
if let Some(ref mut t) = inst.turns_left {
*t = t.saturating_sub(1);
}
}
self.instructions
.retain(|i| i.turns_left.is_none_or(|t| t > 0));
}
pub fn context(&self) -> Option<String> {
if self.instructions.is_empty() {
return None;
}
let mut lines = vec!["# Steering Instructions".to_string()];
for inst in &self.instructions {
let id_short = &inst.id[..8];
let durability = match inst.turns_left {
None => "permanent".to_string(),
Some(1) => "1 turn left".to_string(),
Some(n) => format!("{n} turns left"),
};
lines.push(format!("- [{id_short}] {} ({durability})", inst.text));
}
Some(lines.join("\n"))
}
pub fn list(&self) -> &[SteerInstruction] {
&self.instructions
}
pub fn clear(&mut self) {
self.instructions.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_list_clear() {
let mut mgr = SteerManager::new();
assert!(mgr.list().is_empty());
let id = mgr.add("be concise".to_string(), None);
assert_eq!(mgr.list().len(), 1);
assert_eq!(mgr.list()[0].text, "be concise");
assert!(id.len() > 4);
mgr.clear();
assert!(mgr.list().is_empty());
}
#[test]
fn test_remove() {
let mut mgr = SteerManager::new();
let id = mgr.add("test".to_string(), None);
assert!(mgr.remove(&id[..8])); assert!(mgr.list().is_empty());
assert!(!mgr.remove("nonexistent"));
}
#[test]
fn test_turns_left_decrement_and_expire() {
let mut mgr = SteerManager::new();
mgr.add("temporary".to_string(), Some(2));
mgr.add("permanent".to_string(), None);
mgr.tick();
assert_eq!(mgr.list().len(), 2);
mgr.tick();
assert_eq!(mgr.list().len(), 1);
assert_eq!(mgr.list()[0].text, "permanent");
}
#[test]
fn test_context_empty() {
let mgr = SteerManager::new();
assert!(mgr.context().is_none());
}
#[test]
fn test_context_format() {
let mut mgr = SteerManager::new();
mgr.add("be brief".to_string(), None);
mgr.add("use English".to_string(), Some(3));
let ctx = mgr.context().unwrap();
assert!(ctx.starts_with("# Steering Instructions"));
assert!(ctx.contains("be brief"));
assert!(ctx.contains("permanent"));
assert!(ctx.contains("use English"));
assert!(ctx.contains("3 turns left"));
}
#[test]
fn test_context_single_turn_left() {
let mut mgr = SteerManager::new();
mgr.add("one turn".to_string(), Some(1));
let ctx = mgr.context().unwrap();
assert!(ctx.contains("1 turn left"));
}
}