use rhai::Engine;
pub fn register_types(engine: &mut Engine) {
engine.register_fn("region_pinned", || -> String { "pinned".to_string() });
engine.register_fn("region_temporary", || -> String { "temporary".to_string() });
engine.register_fn("region_clearable", || -> String { "clearable".to_string() });
engine.register_fn("region_sliding_window", |max_items: i64| -> rhai::Map {
let mut map = rhai::Map::new();
map.insert(
"kind".into(),
rhai::Dynamic::from("sliding_window".to_string()),
);
map.insert("max_items".into(), rhai::Dynamic::from(max_items));
map
});
engine.register_fn("region_compacting", |threshold: i64| -> rhai::Map {
let mut map = rhai::Map::new();
map.insert("kind".into(), rhai::Dynamic::from("compacting".to_string()));
map.insert("threshold_tokens".into(), rhai::Dynamic::from(threshold));
map
});
engine.register_fn(
"region_custom",
|script: String, persistent: bool| -> rhai::Map {
let mut map = rhai::Map::new();
map.insert("kind".into(), rhai::Dynamic::from("custom".to_string()));
map.insert("script".into(), rhai::Dynamic::from(script));
map.insert("persistent".into(), rhai::Dynamic::from(persistent));
map
},
);
engine.register_fn(
"region_entry",
|content: String, tokens: i64| -> rhai::Map {
let mut map = rhai::Map::new();
map.insert("content".into(), rhai::Dynamic::from(content));
map.insert("tokens".into(), rhai::Dynamic::from(tokens));
map
},
);
engine.register_fn("content_format", |format: &str| -> String {
match format {
"text" | "json" | "mermaid" | "markdown" | "code" => format.to_string(),
_ => "text".to_string(),
}
});
engine.register_fn(
"tokens_remaining",
|max_tokens: i64, current_tokens: i64| -> i64 { max_tokens.saturating_sub(current_tokens) },
);
engine.register_fn(
"usage_ratio",
|max_tokens: i64, current_tokens: i64| -> f64 {
if max_tokens == 0 {
return 1.0;
}
current_tokens as f64 / max_tokens as f64
},
);
engine.register_fn(
"needs_eviction",
|max_tokens: i64, current_tokens: i64, threshold: f64| -> bool {
if max_tokens == 0 {
return true;
}
(current_tokens as f64 / max_tokens as f64) >= threshold
},
);
}
#[cfg(test)]
mod tests {
use super::*;
use rhai::Engine;
fn engine() -> Engine {
let mut e = Engine::new();
register_types(&mut e);
e
}
#[test]
fn region_pinned_returns_pinned() {
let e = engine();
let result: String = e.eval("region_pinned()").unwrap();
assert_eq!(result, "pinned");
}
#[test]
fn region_temporary_returns_temporary() {
let e = engine();
let result: String = e.eval("region_temporary()").unwrap();
assert_eq!(result, "temporary");
}
#[test]
fn region_clearable_returns_clearable() {
let e = engine();
let result: String = e.eval("region_clearable()").unwrap();
assert_eq!(result, "clearable");
}
#[test]
fn region_sliding_window_returns_map_with_kind_and_max_items() {
let e = engine();
let result: rhai::Map = e.eval("region_sliding_window(10)").unwrap();
assert_eq!(
result.get("kind").unwrap().clone_cast::<String>(),
"sliding_window"
);
assert_eq!(result.get("max_items").unwrap().clone_cast::<i64>(), 10);
}
#[test]
fn region_sliding_window_zero() {
let e = engine();
let result: rhai::Map = e.eval("region_sliding_window(0)").unwrap();
assert_eq!(result.get("max_items").unwrap().clone_cast::<i64>(), 0);
}
#[test]
fn region_compacting_returns_map_with_kind_and_threshold() {
let e = engine();
let result: rhai::Map = e.eval("region_compacting(5000)").unwrap();
assert_eq!(
result.get("kind").unwrap().clone_cast::<String>(),
"compacting"
);
assert_eq!(
result.get("threshold_tokens").unwrap().clone_cast::<i64>(),
5000
);
}
#[test]
fn region_custom_returns_map_with_script_and_persistent() {
let e = engine();
let result: rhai::Map = e.eval(r#"region_custom("hooks/conv.rhai", true)"#).unwrap();
assert_eq!(result.get("kind").unwrap().clone_cast::<String>(), "custom");
assert_eq!(
result.get("script").unwrap().clone_cast::<String>(),
"hooks/conv.rhai"
);
assert!(result.get("persistent").unwrap().clone_cast::<bool>());
}
#[test]
fn region_custom_non_persistent() {
let e = engine();
let result: rhai::Map = e.eval(r#"region_custom("r.rhai", false)"#).unwrap();
assert!(!result.get("persistent").unwrap().clone_cast::<bool>());
}
#[test]
fn region_entry_returns_map_with_content_and_tokens() {
let e = engine();
let result: rhai::Map = e.eval(r#"region_entry("content", 42)"#).unwrap();
assert_eq!(
result.get("content").unwrap().clone_cast::<String>(),
"content"
);
assert_eq!(result.get("tokens").unwrap().clone_cast::<i64>(), 42);
}
#[test]
fn region_entry_empty_content() {
let e = engine();
let result: rhai::Map = e.eval(r#"region_entry("", 0)"#).unwrap();
assert_eq!(result.get("content").unwrap().clone_cast::<String>(), "");
assert_eq!(result.get("tokens").unwrap().clone_cast::<i64>(), 0);
}
#[test]
fn content_format_valid_formats() {
let e = engine();
for fmt in &["text", "json", "mermaid", "markdown", "code"] {
let script = format!(r#"content_format("{fmt}")"#);
let result: String = e.eval(&script).unwrap();
assert_eq!(result, *fmt);
}
}
#[test]
fn content_format_invalid_falls_back_to_text() {
let e = engine();
let result: String = e.eval(r#"content_format("invalid")"#).unwrap();
assert_eq!(result, "text");
}
#[test]
fn content_format_empty_falls_back_to_text() {
let e = engine();
let result: String = e.eval(r#"content_format("")"#).unwrap();
assert_eq!(result, "text");
}
#[test]
fn tokens_remaining_basic() {
let e = engine();
let result: i64 = e.eval("tokens_remaining(100, 30)").unwrap();
assert_eq!(result, 70);
}
#[test]
fn tokens_remaining_zero_used() {
let e = engine();
let result: i64 = e.eval("tokens_remaining(100, 0)").unwrap();
assert_eq!(result, 100);
}
#[test]
fn tokens_remaining_all_used() {
let e = engine();
let result: i64 = e.eval("tokens_remaining(100, 100)").unwrap();
assert_eq!(result, 0);
}
#[test]
fn tokens_remaining_saturates_instead_of_overflowing() {
let e = engine();
let low: i64 = e.eval("tokens_remaining(-9223372036854775808, 1)").unwrap();
assert_eq!(low, i64::MIN);
let high: i64 = e.eval("tokens_remaining(9223372036854775807, -1)").unwrap();
assert_eq!(high, i64::MAX);
}
#[test]
fn usage_ratio_half() {
let e = engine();
let result: f64 = e.eval("usage_ratio(100, 50)").unwrap();
assert!((result - 0.5).abs() < f64::EPSILON);
}
#[test]
fn usage_ratio_zero_max_returns_one() {
let e = engine();
let result: f64 = e.eval("usage_ratio(0, 50)").unwrap();
assert!((result - 1.0).abs() < f64::EPSILON);
}
#[test]
fn usage_ratio_none_used() {
let e = engine();
let result: f64 = e.eval("usage_ratio(100, 0)").unwrap();
assert!((result - 0.0).abs() < f64::EPSILON);
}
#[test]
fn needs_eviction_above_threshold() {
let e = engine();
let result: bool = e.eval("needs_eviction(100, 90, 0.8)").unwrap();
assert!(result);
}
#[test]
fn needs_eviction_below_threshold() {
let e = engine();
let result: bool = e.eval("needs_eviction(100, 50, 0.8)").unwrap();
assert!(!result);
}
#[test]
fn needs_eviction_at_exact_threshold() {
let e = engine();
let result: bool = e.eval("needs_eviction(100, 80, 0.8)").unwrap();
assert!(result);
}
#[test]
fn needs_eviction_zero_max_returns_true() {
let e = engine();
let result: bool = e.eval("needs_eviction(0, 0, 0.8)").unwrap();
assert!(result);
}
}