use crate::state::StateKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThrottleEntry {
pub last_step: usize,
pub content_hash: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContextThrottleMap {
pub entries: HashMap<String, ThrottleEntry>,
}
pub enum ContextThrottleUpdate {
Injected {
key: String,
step: usize,
content_hash: u64,
},
}
pub struct ContextThrottleState;
impl StateKey for ContextThrottleState {
const KEY: &'static str = "__runtime.context_throttle";
type Value = ContextThrottleMap;
type Update = ContextThrottleUpdate;
fn apply(value: &mut Self::Value, update: Self::Update) {
match update {
ContextThrottleUpdate::Injected {
key,
step,
content_hash,
} => {
value.entries.insert(
key,
ThrottleEntry {
last_step: step,
content_hash,
},
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn throttle_map_default_is_empty() {
let map = ContextThrottleMap::default();
assert!(map.entries.is_empty());
}
#[test]
fn injected_creates_entry() {
let mut map = ContextThrottleMap::default();
ContextThrottleState::apply(
&mut map,
ContextThrottleUpdate::Injected {
key: "reminder".into(),
step: 5,
content_hash: 12345,
},
);
assert_eq!(map.entries.len(), 1);
let entry = &map.entries["reminder"];
assert_eq!(entry.last_step, 5);
assert_eq!(entry.content_hash, 12345);
}
#[test]
fn injected_updates_existing_entry() {
let mut map = ContextThrottleMap::default();
ContextThrottleState::apply(
&mut map,
ContextThrottleUpdate::Injected {
key: "reminder".into(),
step: 1,
content_hash: 111,
},
);
ContextThrottleState::apply(
&mut map,
ContextThrottleUpdate::Injected {
key: "reminder".into(),
step: 5,
content_hash: 222,
},
);
assert_eq!(map.entries.len(), 1);
let entry = &map.entries["reminder"];
assert_eq!(entry.last_step, 5);
assert_eq!(entry.content_hash, 222);
}
#[test]
fn injected_multiple_keys_independent() {
let mut map = ContextThrottleMap::default();
ContextThrottleState::apply(
&mut map,
ContextThrottleUpdate::Injected {
key: "a".into(),
step: 1,
content_hash: 100,
},
);
ContextThrottleState::apply(
&mut map,
ContextThrottleUpdate::Injected {
key: "b".into(),
step: 2,
content_hash: 200,
},
);
assert_eq!(map.entries.len(), 2);
assert_eq!(map.entries["a"].last_step, 1);
assert_eq!(map.entries["b"].last_step, 2);
}
#[test]
fn throttle_entry_serde_roundtrip() {
let entry = ThrottleEntry {
last_step: 42,
content_hash: 987654321,
};
let json = serde_json::to_string(&entry).unwrap();
let parsed: ThrottleEntry = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, entry);
}
#[test]
fn throttle_map_serde_roundtrip() {
let mut map = ContextThrottleMap::default();
ContextThrottleState::apply(
&mut map,
ContextThrottleUpdate::Injected {
key: "k1".into(),
step: 3,
content_hash: 456,
},
);
let json = serde_json::to_string(&map).unwrap();
let parsed: ContextThrottleMap = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, map);
}
#[test]
fn content_hash_change_detection() {
let mut map = ContextThrottleMap::default();
ContextThrottleState::apply(
&mut map,
ContextThrottleUpdate::Injected {
key: "reminder".into(),
step: 1,
content_hash: 111,
},
);
let entry = &map.entries["reminder"];
let new_hash: u64 = 222;
let content_changed = entry.content_hash != new_hash;
assert!(content_changed, "different hash should indicate change");
let same_hash: u64 = 111;
let content_same = entry.content_hash == same_hash;
assert!(content_same, "same hash should indicate no change");
}
#[test]
fn cooldown_check_logic() {
let mut map = ContextThrottleMap::default();
ContextThrottleState::apply(
&mut map,
ContextThrottleUpdate::Injected {
key: "reminder".into(),
step: 1,
content_hash: 100,
},
);
let entry = &map.entries["reminder"];
let cooldown = 3usize;
assert!(2usize.saturating_sub(entry.last_step) < cooldown);
assert!(3usize.saturating_sub(entry.last_step) < cooldown);
assert!(4usize.saturating_sub(entry.last_step) >= cooldown);
}
}