use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum EntryKind {
#[default]
Text,
UserMessage,
AssistantTurn { tool_calls: Vec<SerializedToolCall> },
ToolResult {
tool_call_id: String,
tool_name: String,
is_error: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SerializedToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thought_signature: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "strategy", rename_all = "snake_case")]
pub enum EvictionStrategy {
#[default]
PerItem,
Bulk {
overflow: usize,
},
Compact {
compact_count: usize,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RegionKind {
Pinned,
SlidingWindow {
max_items: usize,
eviction_strategy: EvictionStrategy,
},
Temporary,
Compacting {
threshold_tokens: usize,
},
Clearable,
CompactHistory {
source_region: String,
},
HashMap {
max_entries: Option<usize>,
},
Custom {
script: String,
persistent: bool,
},
}
impl PartialEq for RegionKind {
#[inline(never)]
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Pinned, Self::Pinned)
| (Self::Temporary, Self::Temporary)
| (Self::Clearable, Self::Clearable) => true,
(
Self::SlidingWindow {
max_items: a,
eviction_strategy: sa,
},
Self::SlidingWindow {
max_items: b,
eviction_strategy: sb,
},
) => a == b && sa == sb,
(
Self::Compacting {
threshold_tokens: a,
},
Self::Compacting {
threshold_tokens: b,
},
) => a == b,
(
Self::CompactHistory { source_region: a },
Self::CompactHistory { source_region: b },
) => a == b,
(Self::HashMap { max_entries: a }, Self::HashMap { max_entries: b }) => a == b,
(
Self::Custom {
script: a,
persistent: pa,
},
Self::Custom {
script: b,
persistent: pb,
},
) => a == b && pa == pb,
_ => false,
}
}
}
impl Eq for RegionKind {}
impl RegionKind {
pub fn cache_hint(&self) -> crate::cache::CacheHint {
match self {
RegionKind::Pinned | RegionKind::CompactHistory { .. } => {
crate::cache::CacheHint::Always
}
RegionKind::Compacting { .. } => crate::cache::CacheHint::UntilChanged,
RegionKind::SlidingWindow { .. } => crate::cache::CacheHint::SlidingPrefix {
stable_fraction: 0.75,
},
RegionKind::HashMap { .. } => crate::cache::CacheHint::UntilChanged,
RegionKind::Temporary | RegionKind::Clearable => crate::cache::CacheHint::Never,
RegionKind::Custom { persistent, .. } => {
if *persistent {
crate::cache::CacheHint::Always
} else {
crate::cache::CacheHint::UntilChanged
}
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Region {
pub name: String,
pub kind: RegionKind,
pub content: Vec<RegionEntry>,
pub max_tokens: usize,
pub current_tokens: usize,
pub schema: Option<RegionSchema>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub taint: Option<crate::taint::RegionTaint>,
#[serde(default)]
pub needs_message_compaction: bool,
}
impl Region {
pub fn new(name: String, kind: RegionKind, max_tokens: usize) -> Self {
Self {
name,
kind,
content: Vec::new(),
max_tokens,
current_tokens: 0,
schema: None,
taint: None,
needs_message_compaction: false,
}
}
pub fn with_taint_tracking(mut self) -> Self {
self.taint = Some(crate::taint::RegionTaint::new());
self
}
pub fn enable_taint_tracking(&mut self) {
if self.taint.is_none() {
self.taint = Some(crate::taint::RegionTaint::new());
}
}
pub fn taint_level(&self) -> Option<crate::taint::TaintLevel> {
self.taint.as_ref().map(|t| t.level())
}
pub fn add_tainted_entry(
&mut self,
content: String,
tokens: usize,
taint_level: crate::taint::TaintLevel,
) -> crate::error::Result<()> {
if let Some(schema) = &self.schema {
schema.validate(&content)?;
}
if self.current_tokens + tokens > self.max_tokens {
return Err(crate::error::Error::TokenBudgetExceeded {
used: self.current_tokens + tokens,
max: self.max_tokens,
});
}
self.content.push(RegionEntry {
content,
tokens,
timestamp: chrono::Utc::now().timestamp(),
metadata: None,
kind: EntryKind::default(),
key: None,
});
self.current_tokens += tokens;
if let Some(taint) = &mut self.taint {
taint.add_entry(taint_level);
}
self.enforce_sliding_window();
Ok(())
}
pub fn add_typed_tainted_entry(
&mut self,
content: String,
tokens: usize,
kind: EntryKind,
taint_level: crate::taint::TaintLevel,
) -> crate::error::Result<()> {
if let Some(schema) = &self.schema {
schema.validate(&content)?;
}
if self.current_tokens + tokens > self.max_tokens {
return Err(crate::error::Error::TokenBudgetExceeded {
used: self.current_tokens + tokens,
max: self.max_tokens,
});
}
self.content.push(RegionEntry {
content,
tokens,
timestamp: chrono::Utc::now().timestamp(),
metadata: None,
kind,
key: None,
});
self.current_tokens += tokens;
if let Some(taint) = &mut self.taint {
taint.add_entry(taint_level);
}
self.enforce_sliding_window();
Ok(())
}
pub fn with_schema(mut self, schema: RegionSchema) -> Self {
self.schema = Some(schema);
self
}
pub fn add_entry(&mut self, content: String, tokens: usize) -> crate::error::Result<()> {
if let Some(schema) = &self.schema {
schema.validate(&content)?;
}
if self.current_tokens + tokens > self.max_tokens {
return Err(crate::error::Error::TokenBudgetExceeded {
used: self.current_tokens + tokens,
max: self.max_tokens,
});
}
self.content.push(RegionEntry {
content,
tokens,
timestamp: chrono::Utc::now().timestamp(),
metadata: None,
kind: EntryKind::default(),
key: None,
});
self.current_tokens += tokens;
if let Some(taint) = &mut self.taint {
taint.add_entry(crate::taint::TaintLevel::Public);
}
self.enforce_sliding_window();
Ok(())
}
pub fn add_entry_with_metadata(
&mut self,
content: String,
tokens: usize,
metadata: serde_json::Value,
) -> crate::error::Result<()> {
if let Some(schema) = &self.schema {
schema.validate(&content)?;
}
if self.current_tokens + tokens > self.max_tokens {
return Err(crate::error::Error::TokenBudgetExceeded {
used: self.current_tokens + tokens,
max: self.max_tokens,
});
}
self.content.push(RegionEntry {
content,
tokens,
timestamp: chrono::Utc::now().timestamp(),
metadata: Some(metadata),
kind: EntryKind::default(),
key: None,
});
self.current_tokens += tokens;
if let Some(taint) = &mut self.taint {
taint.add_entry(crate::taint::TaintLevel::Public);
}
self.enforce_sliding_window();
Ok(())
}
pub fn add_typed_entry(
&mut self,
content: String,
tokens: usize,
kind: EntryKind,
) -> crate::error::Result<()> {
if let Some(schema) = &self.schema {
schema.validate(&content)?;
}
if self.current_tokens + tokens > self.max_tokens {
return Err(crate::error::Error::TokenBudgetExceeded {
used: self.current_tokens + tokens,
max: self.max_tokens,
});
}
self.content.push(RegionEntry {
content,
tokens,
timestamp: chrono::Utc::now().timestamp(),
metadata: None,
kind,
key: None,
});
self.current_tokens += tokens;
if let Some(taint) = &mut self.taint {
taint.add_entry(crate::taint::TaintLevel::Public);
}
self.enforce_sliding_window();
Ok(())
}
pub fn carry_entry(&mut self, entry: RegionEntry) -> crate::error::Result<()> {
if self.current_tokens + entry.tokens > self.max_tokens {
return Err(crate::error::Error::TokenBudgetExceeded {
used: self.current_tokens + entry.tokens,
max: self.max_tokens,
});
}
self.current_tokens += entry.tokens;
self.content.push(entry);
self.enforce_sliding_window();
Ok(())
}
pub fn upsert_by_key(
&mut self,
key: &str,
content: String,
tokens: usize,
) -> Result<(), String> {
if let Some(pos) = self
.content
.iter()
.position(|e| e.key.as_deref() == Some(key))
{
let old_tokens = self.content[pos].tokens;
self.current_tokens -= old_tokens;
self.content[pos].content = content;
self.content[pos].tokens = tokens;
self.content[pos].timestamp = chrono::Utc::now().timestamp();
self.current_tokens += tokens;
return Ok(());
}
let max_entries = if let RegionKind::HashMap {
max_entries: Some(max),
} = &self.kind
{
Some(*max)
} else {
None
};
if let Some(max) = max_entries {
while self.content.len() >= max {
self.evict_lru_entry();
}
}
while self.current_tokens + tokens > self.max_tokens && !self.content.is_empty() {
self.evict_lru_entry();
}
if self.current_tokens + tokens > self.max_tokens {
return Err(format!(
"Entry ({} tokens) exceeds region budget ({} max)",
tokens, self.max_tokens
));
}
self.content.push(RegionEntry {
content,
tokens,
timestamp: chrono::Utc::now().timestamp(),
metadata: None,
kind: EntryKind::default(),
key: Some(key.to_string()),
});
self.current_tokens += tokens;
Ok(())
}
pub fn get_by_key(&self, key: &str) -> Option<&RegionEntry> {
self.content.iter().find(|e| e.key.as_deref() == Some(key))
}
pub fn remove_by_key(&mut self, key: &str) -> bool {
if let Some(pos) = self
.content
.iter()
.position(|e| e.key.as_deref() == Some(key))
{
let tokens = self.content[pos].tokens;
self.content.remove(pos);
self.current_tokens -= tokens;
if let Some(taint) = &mut self.taint {
taint.remove_at(pos);
}
true
} else {
false
}
}
pub fn keys(&self) -> Vec<&str> {
self.content
.iter()
.filter_map(|e| e.key.as_deref())
.collect()
}
fn evict_lru_entry(&mut self) {
if self.content.is_empty() {
return;
}
let oldest_idx = self
.content
.iter()
.enumerate()
.min_by_key(|(_, e)| e.timestamp)
.map(|(i, _)| i)
.unwrap_or(0);
let tokens = self.content[oldest_idx].tokens;
self.content.remove(oldest_idx);
self.current_tokens -= tokens;
if let Some(taint) = &mut self.taint {
taint.remove_at(oldest_idx);
}
}
fn enforce_sliding_window(&mut self) {
if let RegionKind::SlidingWindow {
max_items,
eviction_strategy,
} = &self.kind
{
let max = *max_items;
match eviction_strategy.clone() {
EvictionStrategy::PerItem => {
while self.content.len() > max && self.remove_oldest().is_some() {}
}
EvictionStrategy::Bulk { overflow } => {
if self.content.len() > max + overflow {
while self.content.len() > max && self.remove_oldest().is_some() {}
}
}
EvictionStrategy::Compact { compact_count } => {
if self.content.len() > max + compact_count * 2 {
while self.content.len() > max && self.remove_oldest().is_some() {}
self.needs_message_compaction = false;
} else if self.content.len() > max + compact_count {
self.needs_message_compaction = true;
}
}
}
}
}
fn turn_group_size_at(&self, idx: usize) -> usize {
if idx >= self.content.len() {
return 0;
}
match &self.content[idx].kind {
EntryKind::AssistantTurn { .. } => {
let mut size = 1;
while idx + size < self.content.len() {
if matches!(self.content[idx + size].kind, EntryKind::ToolResult { .. }) {
size += 1;
} else {
break;
}
}
size
}
_ => 1,
}
}
pub fn clear(&mut self) {
self.content.clear();
self.current_tokens = 0;
if let Some(taint) = &mut self.taint {
taint.clear();
}
}
pub fn remove_oldest(&mut self) -> Option<RegionEntry> {
if self.content.is_empty() {
return None;
}
let group_size = self.turn_group_size_at(0);
let mut first = None;
let mut extra_tokens = 0usize;
let mut i = 0;
while i < group_size && !self.content.is_empty() {
let entry_tokens = self.content[0].tokens;
self.current_tokens -= entry_tokens;
let removed = self.content.remove(0);
if let Some(taint) = &mut self.taint {
taint.remove_oldest();
}
if i == 0 {
first = Some(removed);
} else {
extra_tokens += entry_tokens;
}
i += 1;
}
first.map(|mut entry| {
entry.tokens += extra_tokens;
entry
})
}
pub fn remove_entries_by_prefix(&mut self, prefix: &str) {
let mut i = 0;
while i < self.content.len() {
if self.content[i].content.starts_with(prefix) {
let tokens = self.content[i].tokens;
self.content.remove(i);
self.current_tokens -= tokens;
if let Some(taint) = &mut self.taint {
taint.remove_at(i);
}
} else {
i += 1;
}
}
}
pub fn entry_count(&self) -> usize {
self.content.len()
}
pub fn needs_compaction(&self) -> bool {
if let RegionKind::Compacting { threshold_tokens } = self.kind {
self.current_tokens > threshold_tokens
} else {
false
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegionEntry {
pub content: String,
pub tokens: usize,
pub timestamp: i64,
pub metadata: Option<serde_json::Value>,
#[serde(default)]
pub kind: EntryKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RegionSchema {
pub format: ContentFormat,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_script: Option<String>,
}
impl Clone for RegionSchema {
fn clone(&self) -> Self {
Self {
format: self.format.clone(),
custom_script: self.custom_script.clone(),
}
}
}
impl RegionSchema {
pub fn new(format: ContentFormat) -> Self {
Self {
format,
custom_script: None,
}
}
pub fn with_custom_script(mut self, script: String) -> Self {
self.custom_script = Some(script);
self
}
pub fn validate(&self, content: &str) -> crate::error::Result<()> {
match &self.format {
ContentFormat::Json => {
serde_json::from_str::<serde_json::Value>(content).map_err(|e| {
crate::error::Error::ValidationFailed(format!("Invalid JSON: {}", e))
})?;
}
ContentFormat::Mermaid => {
if !content.contains("graph")
&& !content.contains("sequenceDiagram")
&& !content.contains("classDiagram")
&& !content.contains("stateDiagram")
&& !content.contains("erDiagram")
&& !content.contains("journey")
&& !content.contains("gantt")
&& !content.contains("pie")
&& !content.contains("flowchart")
{
return Err(crate::error::Error::ValidationFailed(
"Mermaid diagrams must contain a valid diagram type (graph, sequenceDiagram, etc.)".to_string()
));
}
}
ContentFormat::Code { .. } => {
if content.trim().is_empty() {
return Err(crate::error::Error::ValidationFailed(
"Code cannot be empty".to_string(),
));
}
}
ContentFormat::Markdown => {
if content.trim().is_empty() {
return Err(crate::error::Error::ValidationFailed(
"Markdown content cannot be empty".to_string(),
));
}
}
ContentFormat::Text | ContentFormat::Custom { .. } => {
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ContentFormat {
Text,
Json,
Mermaid,
Code { language: String },
Markdown,
Custom { format_name: String },
}
pub trait Validator: Send + Sync {
fn validate(&self, content: &str) -> std::result::Result<(), crate::error::ValidationError>;
fn description(&self) -> &str;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_region_creation() {
let region = Region::new("test".to_string(), RegionKind::Pinned, 1000);
assert_eq!(region.name, "test");
assert_eq!(region.max_tokens, 1000);
assert_eq!(region.current_tokens, 0);
}
#[test]
fn test_sliding_window_config() {
let kind = RegionKind::SlidingWindow {
max_items: 10,
eviction_strategy: EvictionStrategy::PerItem,
};
let region = Region::new("history".to_string(), kind.clone(), 5000);
assert_eq!(region.kind, kind);
}
#[test]
fn test_region_kind_equality() {
assert_eq!(RegionKind::Clearable, RegionKind::Clearable);
assert_eq!(
RegionKind::Compacting {
threshold_tokens: 500
},
RegionKind::Compacting {
threshold_tokens: 500
}
);
assert_eq!(
RegionKind::CompactHistory {
source_region: "conv".to_string()
},
RegionKind::CompactHistory {
source_region: "conv".to_string()
}
);
assert_ne!(RegionKind::Pinned, RegionKind::Temporary);
}
#[test]
fn custom_kind_equality_compares_script_and_persistent() {
let a = RegionKind::Custom {
script: "conv.rhai".to_string(),
persistent: false,
};
assert_eq!(a, a.clone());
assert_ne!(
a,
RegionKind::Custom {
script: "other.rhai".to_string(),
persistent: false,
}
);
assert_ne!(
a,
RegionKind::Custom {
script: "conv.rhai".to_string(),
persistent: true,
}
);
assert_ne!(a, RegionKind::Temporary);
}
#[test]
fn custom_kind_serde_round_trips() {
let kind = RegionKind::Custom {
script: "hooks/conv.rhai".to_string(),
persistent: true,
};
let json = serde_json::to_string(&kind).unwrap();
let back: RegionKind = serde_json::from_str(&json).unwrap();
assert_eq!(kind, back);
let old: RegionKind = serde_json::from_str("\"Pinned\"").unwrap();
assert_eq!(old, RegionKind::Pinned);
}
#[test]
fn custom_kind_cache_hint_follows_persistent() {
assert_eq!(
RegionKind::Custom {
script: "s.rhai".to_string(),
persistent: true,
}
.cache_hint(),
crate::cache::CacheHint::Always
);
assert_eq!(
RegionKind::Custom {
script: "s.rhai".to_string(),
persistent: false,
}
.cache_hint(),
crate::cache::CacheHint::UntilChanged
);
}
#[test]
fn carry_entry_preserves_kind_metadata_key_and_timestamp() {
let mut source = Region::new("conversation".to_string(), RegionKind::Temporary, 10_000);
source
.add_typed_entry(
"result body".to_string(),
10,
EntryKind::ToolResult {
tool_call_id: "call_1".to_string(),
tool_name: "read_file".to_string(),
is_error: false,
},
)
.unwrap();
let mut entry = source.content[0].clone();
entry.metadata = Some(serde_json::json!({"origin": "test"}));
entry.key = Some("k".to_string());
let stamped = entry.timestamp;
let mut dest = Region::new("conversation".to_string(), RegionKind::Temporary, 10_000);
dest.carry_entry(entry).unwrap();
let carried = &dest.content[0];
assert!(matches!(
&carried.kind,
EntryKind::ToolResult { tool_call_id, .. } if tool_call_id == "call_1"
));
assert_eq!(
carried.metadata,
Some(serde_json::json!({"origin": "test"}))
);
assert_eq!(carried.key.as_deref(), Some("k"));
assert_eq!(carried.timestamp, stamped);
assert_eq!(dest.current_tokens, 10);
}
#[test]
fn carry_entry_rejects_over_budget() {
let mut dest = Region::new("small".to_string(), RegionKind::Temporary, 5);
let mut source = Region::new("src".to_string(), RegionKind::Temporary, 100);
source.add_entry("filler".to_string(), 10).unwrap();
let err = dest.carry_entry(source.content[0].clone()).unwrap_err();
assert_eq!(err.to_string(), "Content exceeds token budget: 10 > 5");
assert!(dest.content.is_empty());
assert_eq!(dest.current_tokens, 0);
}
#[test]
fn carry_entry_enforces_sliding_window_max_items() {
let mut source = Region::new("src".to_string(), RegionKind::Temporary, 10_000);
for i in 0..4 {
source.add_entry(format!("msg{i}"), 10).unwrap();
}
let mut dest = Region::new(
"conv".to_string(),
RegionKind::SlidingWindow {
max_items: 3,
eviction_strategy: EvictionStrategy::PerItem,
},
10_000,
);
for entry in &source.content {
dest.carry_entry(entry.clone()).unwrap();
}
assert_eq!(dest.content.len(), 3);
assert_eq!(dest.content[0].content, "msg1");
}
#[test]
fn test_sliding_window_enforces_max_items() {
let mut region = Region::new(
"conv".to_string(),
RegionKind::SlidingWindow {
max_items: 3,
eviction_strategy: EvictionStrategy::PerItem,
},
50000,
);
region.add_entry("msg1".to_string(), 10).unwrap();
region.add_entry("msg2".to_string(), 20).unwrap();
region.add_entry("msg3".to_string(), 30).unwrap();
assert_eq!(region.entry_count(), 3);
assert_eq!(region.current_tokens, 60);
region.add_entry("msg4".to_string(), 40).unwrap();
assert_eq!(region.entry_count(), 3);
assert_eq!(region.content[0].content, "msg2");
assert_eq!(region.content[2].content, "msg4");
assert_eq!(region.current_tokens, 90);
region.add_entry("msg5".to_string(), 50).unwrap();
assert_eq!(region.entry_count(), 3);
assert_eq!(region.content[0].content, "msg3");
assert_eq!(region.current_tokens, 120); }
#[test]
fn test_sliding_window_enforces_max_items_with_metadata() {
let mut region = Region::new(
"conv".to_string(),
RegionKind::SlidingWindow {
max_items: 2,
eviction_strategy: EvictionStrategy::PerItem,
},
50000,
);
region
.add_entry_with_metadata("a".to_string(), 10, serde_json::json!({"idx": 1}))
.unwrap();
region
.add_entry_with_metadata("b".to_string(), 20, serde_json::json!({"idx": 2}))
.unwrap();
region
.add_entry_with_metadata("c".to_string(), 30, serde_json::json!({"idx": 3}))
.unwrap();
assert_eq!(region.entry_count(), 2);
assert_eq!(region.content[0].content, "b");
assert_eq!(region.content[1].content, "c");
assert_eq!(region.current_tokens, 50);
}
#[test]
fn test_cache_hint_pinned() {
let kind = RegionKind::Pinned;
assert_eq!(kind.cache_hint(), crate::cache::CacheHint::Always);
}
#[test]
fn test_cache_hint_compact_history() {
let kind = RegionKind::CompactHistory {
source_region: "conv".to_string(),
};
assert_eq!(kind.cache_hint(), crate::cache::CacheHint::Always);
}
#[test]
fn test_cache_hint_compacting() {
let kind = RegionKind::Compacting {
threshold_tokens: 1000,
};
assert_eq!(kind.cache_hint(), crate::cache::CacheHint::UntilChanged);
}
#[test]
fn test_cache_hint_sliding_window() {
let kind = RegionKind::SlidingWindow {
max_items: 10,
eviction_strategy: EvictionStrategy::PerItem,
};
assert_eq!(
kind.cache_hint(),
crate::cache::CacheHint::SlidingPrefix {
stable_fraction: 0.75
}
);
}
#[test]
fn test_cache_hint_temporary() {
assert_eq!(
RegionKind::Temporary.cache_hint(),
crate::cache::CacheHint::Never
);
}
#[test]
fn test_cache_hint_clearable() {
assert_eq!(
RegionKind::Clearable.cache_hint(),
crate::cache::CacheHint::Never
);
}
#[test]
fn test_with_schema_attaches_schema() {
let schema = RegionSchema::new(ContentFormat::Json);
let region =
Region::new("data".to_string(), RegionKind::Temporary, 1000).with_schema(schema);
assert!(region.schema.is_some());
}
#[test]
fn test_add_entry_rejects_content_failing_schema() {
let schema = RegionSchema::new(ContentFormat::Json);
let mut region =
Region::new("data".to_string(), RegionKind::Temporary, 1000).with_schema(schema);
let result = region.add_entry("not json".to_string(), 10);
assert!(result.is_err());
assert_eq!(region.entry_count(), 0);
}
#[test]
fn test_add_entry_accepts_content_passing_schema() {
let schema = RegionSchema::new(ContentFormat::Json);
let mut region =
Region::new("data".to_string(), RegionKind::Temporary, 1000).with_schema(schema);
let result = region.add_entry("{\"a\":1}".to_string(), 10);
assert!(result.is_ok());
assert_eq!(region.entry_count(), 1);
}
#[test]
fn test_add_entry_rejects_over_budget() {
let mut region = Region::new("data".to_string(), RegionKind::Temporary, 10);
let result = region.add_entry("too much".to_string(), 20);
assert_eq!(
result.unwrap_err().to_string(),
"Content exceeds token budget: 20 > 10"
);
assert_eq!(region.entry_count(), 0);
}
#[test]
fn test_add_entry_with_metadata_rejects_content_failing_schema() {
let schema = RegionSchema::new(ContentFormat::Json);
let mut region =
Region::new("data".to_string(), RegionKind::Temporary, 1000).with_schema(schema);
let result =
region.add_entry_with_metadata("not json".to_string(), 10, serde_json::json!({}));
assert!(result.is_err());
}
#[test]
fn test_add_entry_with_metadata_rejects_over_budget() {
let mut region = Region::new("data".to_string(), RegionKind::Temporary, 10);
let result =
region.add_entry_with_metadata("too much".to_string(), 20, serde_json::json!({}));
assert_eq!(
result.unwrap_err().to_string(),
"Content exceeds token budget: 20 > 10"
);
}
#[test]
fn test_add_entry_with_metadata_stores_metadata() {
let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
region
.add_entry_with_metadata("hello".to_string(), 5, serde_json::json!({"k": "v"}))
.unwrap();
assert_eq!(
region.content[0].metadata,
Some(serde_json::json!({"k": "v"}))
);
}
#[test]
fn test_clear_removes_all_content_and_resets_tokens() {
let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
region.add_entry("a".to_string(), 10).unwrap();
region.add_entry("b".to_string(), 20).unwrap();
assert_eq!(region.entry_count(), 2);
region.clear();
assert_eq!(region.entry_count(), 0);
assert_eq!(region.current_tokens, 0);
}
#[test]
fn test_remove_oldest_returns_and_removes_first_entry() {
let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
region.add_entry("first".to_string(), 10).unwrap();
region.add_entry("second".to_string(), 20).unwrap();
let removed = region.remove_oldest().unwrap();
assert_eq!(removed.content, "first");
assert_eq!(region.entry_count(), 1);
assert_eq!(region.current_tokens, 20);
}
#[test]
fn test_remove_oldest_returns_none_when_empty() {
let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
assert!(region.remove_oldest().is_none());
}
#[test]
fn test_needs_compaction_true_when_over_threshold() {
let mut region = Region::new(
"impl".to_string(),
RegionKind::Compacting {
threshold_tokens: 10,
},
1000,
);
region.add_entry("x".to_string(), 20).unwrap();
assert!(region.needs_compaction());
}
#[test]
fn test_needs_compaction_false_when_under_threshold() {
let mut region = Region::new(
"impl".to_string(),
RegionKind::Compacting {
threshold_tokens: 100,
},
1000,
);
region.add_entry("x".to_string(), 20).unwrap();
assert!(!region.needs_compaction());
}
#[test]
fn test_needs_compaction_false_for_non_compacting_kind() {
let region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
assert!(!region.needs_compaction());
}
#[test]
fn test_region_schema_with_custom_script() {
let schema = RegionSchema::new(ContentFormat::Custom {
format_name: "special".to_string(),
})
.with_custom_script("validate_special()".to_string());
assert_eq!(schema.custom_script.as_deref(), Some("validate_special()"));
}
#[test]
fn test_validate_json_valid() {
let schema = RegionSchema::new(ContentFormat::Json);
assert!(schema.validate("{\"a\": 1}").is_ok());
}
#[test]
fn test_validate_json_invalid() {
let schema = RegionSchema::new(ContentFormat::Json);
let err = schema.validate("not json").unwrap_err();
assert!(err.to_string().starts_with("Region validation failed:"));
}
#[test]
fn test_validate_mermaid_valid() {
let schema = RegionSchema::new(ContentFormat::Mermaid);
assert!(schema.validate("graph TD\nA-->B").is_ok());
}
#[test]
fn test_validate_mermaid_all_recognized_diagram_types() {
let schema = RegionSchema::new(ContentFormat::Mermaid);
for kind in [
"graph",
"sequenceDiagram",
"classDiagram",
"stateDiagram",
"erDiagram",
"journey",
"gantt",
"pie",
"flowchart",
] {
assert!(schema.validate(&format!("{} content", kind)).is_ok());
}
}
#[test]
fn test_validate_mermaid_invalid() {
let schema = RegionSchema::new(ContentFormat::Mermaid);
let err = schema.validate("just some text").unwrap_err();
assert!(err.to_string().starts_with("Region validation failed:"));
}
#[test]
fn test_validate_code_non_empty_is_ok() {
let schema = RegionSchema::new(ContentFormat::Code {
language: "rust".to_string(),
});
assert!(schema.validate("fn main() {}").is_ok());
}
#[test]
fn test_validate_code_empty_is_error() {
let schema = RegionSchema::new(ContentFormat::Code {
language: "rust".to_string(),
});
let err = schema.validate(" ").unwrap_err();
assert!(err.to_string().starts_with("Region validation failed:"));
}
#[test]
fn test_validate_markdown_non_empty_is_ok() {
let schema = RegionSchema::new(ContentFormat::Markdown);
assert!(schema.validate("# Heading").is_ok());
}
#[test]
fn test_validate_markdown_empty_is_error() {
let schema = RegionSchema::new(ContentFormat::Markdown);
let err = schema.validate("").unwrap_err();
assert!(err.to_string().starts_with("Region validation failed:"));
}
#[test]
fn test_validate_text_has_no_restrictions() {
let schema = RegionSchema::new(ContentFormat::Text);
assert!(schema.validate("").is_ok());
assert!(schema.validate("anything at all").is_ok());
}
#[test]
fn test_validate_custom_has_no_restrictions_here() {
let schema = RegionSchema::new(ContentFormat::Custom {
format_name: "special".to_string(),
});
assert!(schema.validate("").is_ok());
assert!(schema.validate("whatever").is_ok());
}
#[test]
fn test_region_schema_clone_preserves_fields() {
let schema = RegionSchema::new(ContentFormat::Text).with_custom_script("s".to_string());
let cloned = schema.clone();
assert_eq!(cloned.custom_script.as_deref(), Some("s"));
assert_eq!(cloned.format, ContentFormat::Text);
}
#[test]
fn test_region_with_taint_tracking() {
let region =
Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
assert!(region.taint.is_some());
assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
}
#[test]
fn test_region_without_taint_tracking() {
let region = Region::new("test".to_string(), RegionKind::Temporary, 1000);
assert!(region.taint.is_none());
assert_eq!(region.taint_level(), None);
}
#[test]
fn test_enable_taint_tracking() {
let mut region = Region::new("test".to_string(), RegionKind::Temporary, 1000);
assert!(region.taint.is_none());
region.enable_taint_tracking();
assert!(region.taint.is_some());
region.enable_taint_tracking();
assert!(region.taint.is_some());
}
#[test]
fn test_add_tainted_entry() {
let mut region =
Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
region
.add_tainted_entry(
"secret data".to_string(),
10,
crate::taint::TaintLevel::Private,
)
.unwrap();
assert_eq!(
region.taint_level(),
Some(crate::taint::TaintLevel::Private)
);
assert_eq!(region.entry_count(), 1);
}
#[test]
fn test_add_tainted_entry_validates_schema() {
let mut region = Region::new("test".to_string(), RegionKind::Temporary, 1000)
.with_taint_tracking()
.with_schema(RegionSchema::new(ContentFormat::Json));
let result = region.add_tainted_entry(
"not json".to_string(),
10,
crate::taint::TaintLevel::Internal,
);
assert!(result.is_err());
assert_eq!(region.entry_count(), 0);
}
#[test]
fn test_add_tainted_entry_checks_budget() {
let mut region =
Region::new("test".to_string(), RegionKind::Temporary, 10).with_taint_tracking();
let result = region.add_tainted_entry(
"too much".to_string(),
20,
crate::taint::TaintLevel::Internal,
);
assert!(result.is_err());
}
#[test]
fn test_add_entry_tracks_taint_as_public() {
let mut region =
Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
region.add_entry("public data".to_string(), 10).unwrap();
assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
}
#[test]
fn test_taint_recovery_on_remove_oldest() {
let mut region =
Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
region
.add_tainted_entry("private".to_string(), 10, crate::taint::TaintLevel::Private)
.unwrap();
region
.add_tainted_entry("public".to_string(), 10, crate::taint::TaintLevel::Public)
.unwrap();
assert_eq!(
region.taint_level(),
Some(crate::taint::TaintLevel::Private)
);
region.remove_oldest(); assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
}
#[test]
fn test_taint_recovery_on_clear() {
let mut region =
Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
region
.add_tainted_entry("private".to_string(), 10, crate::taint::TaintLevel::Private)
.unwrap();
region.clear();
assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
}
#[test]
fn test_taint_recovery_on_sliding_window_eviction() {
let mut region = Region::new(
"conv".to_string(),
RegionKind::SlidingWindow {
max_items: 2,
eviction_strategy: EvictionStrategy::PerItem,
},
50000,
)
.with_taint_tracking();
region
.add_tainted_entry("private".to_string(), 10, crate::taint::TaintLevel::Private)
.unwrap();
region
.add_tainted_entry("public1".to_string(), 10, crate::taint::TaintLevel::Public)
.unwrap();
assert_eq!(
region.taint_level(),
Some(crate::taint::TaintLevel::Private)
);
region
.add_tainted_entry("public2".to_string(), 10, crate::taint::TaintLevel::Public)
.unwrap();
assert_eq!(region.entry_count(), 2);
assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
}
#[test]
fn test_taint_field_not_serialized_when_none() {
let region = Region::new("test".to_string(), RegionKind::Temporary, 1000);
let json = serde_json::to_string(®ion).unwrap();
assert!(!json.contains("taint"));
}
#[test]
fn test_taint_field_deserialized_as_none_when_missing() {
let json = r#"{"name":"test","kind":"Temporary","content":[],"max_tokens":1000,"current_tokens":0,"schema":null}"#;
let region: Region = serde_json::from_str(json).unwrap();
assert!(region.taint.is_none());
}
#[test]
fn test_add_typed_tainted_entry() {
let mut region = Region::new(
"conversation".to_string(),
RegionKind::SlidingWindow {
max_items: 100,
eviction_strategy: EvictionStrategy::PerItem,
},
1000,
)
.with_taint_tracking();
region
.add_typed_tainted_entry(
"secret data".to_string(),
10,
EntryKind::ToolResult {
tool_call_id: "tc_1".to_string(),
tool_name: "calendar".to_string(),
is_error: false,
},
crate::taint::TaintLevel::Private,
)
.unwrap();
assert_eq!(region.content.len(), 1);
assert_eq!(
region.content[0].kind,
EntryKind::ToolResult {
tool_call_id: "tc_1".to_string(),
tool_name: "calendar".to_string(),
is_error: false,
}
);
assert_eq!(
region.taint_level(),
Some(crate::taint::TaintLevel::Private)
);
}
#[test]
fn serialized_tool_call_round_trips_thought_signature_and_reads_old_json() {
let with = SerializedToolCall {
id: "c1".into(),
name: "shell".into(),
arguments: serde_json::json!({"command": "ls"}),
thought_signature: Some("sig".into()),
};
let json = serde_json::to_string(&with).unwrap();
let back: SerializedToolCall = serde_json::from_str(&json).unwrap();
assert_eq!(back.thought_signature.as_deref(), Some("sig"));
let old = r#"{"id":"c2","name":"shell","arguments":{}}"#;
let back: SerializedToolCall = serde_json::from_str(old).unwrap();
assert_eq!(back.thought_signature, None);
let without = SerializedToolCall {
id: "c3".into(),
name: "shell".into(),
arguments: serde_json::json!({}),
thought_signature: None,
};
assert!(
!serde_json::to_string(&without)
.unwrap()
.contains("thought_signature")
);
}
#[test]
fn test_add_typed_tainted_entry_checks_budget() {
let mut region = Region::new(
"conversation".to_string(),
RegionKind::SlidingWindow {
max_items: 100,
eviction_strategy: EvictionStrategy::PerItem,
},
5,
)
.with_taint_tracking();
let result = region.add_typed_tainted_entry(
"too large".to_string(),
100,
EntryKind::ToolResult {
tool_call_id: "tc_1".to_string(),
tool_name: "tool".to_string(),
is_error: false,
},
crate::taint::TaintLevel::Internal,
);
assert!(result.is_err());
}
#[test]
fn test_add_typed_tainted_entry_validates_schema() {
let mut region = Region::new("test".to_string(), RegionKind::Pinned, 1000)
.with_taint_tracking()
.with_schema(RegionSchema::new(ContentFormat::Json));
let result = region.add_typed_tainted_entry(
"not json".to_string(),
5,
EntryKind::Text,
crate::taint::TaintLevel::Public,
);
assert!(result.is_err());
}
#[test]
fn test_add_typed_tainted_entry_without_taint_tracking() {
let mut region = Region::new(
"conversation".to_string(),
RegionKind::SlidingWindow {
max_items: 100,
eviction_strategy: EvictionStrategy::PerItem,
},
1000,
);
region
.add_typed_tainted_entry(
"data".to_string(),
10,
EntryKind::Text,
crate::taint::TaintLevel::Private,
)
.unwrap();
assert_eq!(region.content.len(), 1);
assert_eq!(region.taint_level(), None); }
#[test]
fn test_turn_group_size_at_assistant_with_tool_results() {
let mut region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
region
.add_typed_entry(
"assistant response".to_string(),
10,
EntryKind::AssistantTurn {
tool_calls: vec![
SerializedToolCall {
id: "tc_1".to_string(),
name: "read_file".to_string(),
arguments: serde_json::json!({}),
thought_signature: None,
},
SerializedToolCall {
id: "tc_2".to_string(),
name: "write_file".to_string(),
arguments: serde_json::json!({}),
thought_signature: None,
},
],
},
)
.unwrap();
region
.add_typed_entry(
"result 1".to_string(),
5,
EntryKind::ToolResult {
tool_call_id: "tc_1".to_string(),
tool_name: "read_file".to_string(),
is_error: false,
},
)
.unwrap();
region
.add_typed_entry(
"result 2".to_string(),
5,
EntryKind::ToolResult {
tool_call_id: "tc_2".to_string(),
tool_name: "write_file".to_string(),
is_error: false,
},
)
.unwrap();
assert_eq!(region.turn_group_size_at(0), 3);
}
#[test]
fn test_turn_group_size_at_assistant_at_end() {
let mut region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
region
.add_typed_entry(
"assistant with no tools".to_string(),
10,
EntryKind::AssistantTurn { tool_calls: vec![] },
)
.unwrap();
assert_eq!(region.turn_group_size_at(0), 1);
}
#[test]
fn test_turn_group_size_at_out_of_bounds() {
let region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
assert_eq!(region.turn_group_size_at(0), 0);
assert_eq!(region.turn_group_size_at(99), 0);
}
#[test]
fn test_turn_group_size_at_non_assistant_entries() {
let mut region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
region
.add_typed_entry("hello".to_string(), 5, EntryKind::Text)
.unwrap();
region
.add_typed_entry("hi".to_string(), 5, EntryKind::UserMessage)
.unwrap();
region
.add_typed_entry(
"orphan result".to_string(),
5,
EntryKind::ToolResult {
tool_call_id: "tc_x".to_string(),
tool_name: "tool".to_string(),
is_error: false,
},
)
.unwrap();
assert_eq!(region.turn_group_size_at(0), 1); assert_eq!(region.turn_group_size_at(1), 1); assert_eq!(region.turn_group_size_at(2), 1); }
#[test]
fn test_remove_oldest_evicts_entire_turn_group() {
let mut region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
region
.add_typed_entry(
"assistant".to_string(),
100,
EntryKind::AssistantTurn {
tool_calls: vec![
SerializedToolCall {
id: "tc_1".to_string(),
name: "read_file".to_string(),
arguments: serde_json::json!({}),
thought_signature: None,
},
SerializedToolCall {
id: "tc_2".to_string(),
name: "list_dir".to_string(),
arguments: serde_json::json!({}),
thought_signature: None,
},
],
},
)
.unwrap();
region
.add_typed_entry(
"result 1".to_string(),
30,
EntryKind::ToolResult {
tool_call_id: "tc_1".to_string(),
tool_name: "read_file".to_string(),
is_error: false,
},
)
.unwrap();
region
.add_typed_entry(
"result 2".to_string(),
20,
EntryKind::ToolResult {
tool_call_id: "tc_2".to_string(),
tool_name: "list_dir".to_string(),
is_error: false,
},
)
.unwrap();
region
.add_typed_entry("user msg".to_string(), 10, EntryKind::UserMessage)
.unwrap();
assert_eq!(region.entry_count(), 4);
assert_eq!(region.current_tokens, 160);
let removed = region.remove_oldest().unwrap();
assert_eq!(removed.content, "assistant");
assert_eq!(removed.tokens, 100 + 30 + 20); assert_eq!(region.entry_count(), 1);
assert_eq!(region.content[0].content, "user msg");
assert_eq!(region.current_tokens, 10);
}
#[test]
fn test_remove_oldest_turn_group_calls_taint_remove_for_each_entry() {
let mut region =
Region::new("conv".to_string(), RegionKind::Temporary, 50000).with_taint_tracking();
region
.add_typed_tainted_entry(
"assistant".to_string(),
10,
EntryKind::AssistantTurn {
tool_calls: vec![SerializedToolCall {
id: "tc_1".to_string(),
name: "tool".to_string(),
arguments: serde_json::json!({}),
thought_signature: None,
}],
},
crate::taint::TaintLevel::Private,
)
.unwrap();
region
.add_typed_tainted_entry(
"result".to_string(),
5,
EntryKind::ToolResult {
tool_call_id: "tc_1".to_string(),
tool_name: "tool".to_string(),
is_error: false,
},
crate::taint::TaintLevel::Internal,
)
.unwrap();
region
.add_tainted_entry(
"public stuff".to_string(),
5,
crate::taint::TaintLevel::Public,
)
.unwrap();
assert_eq!(
region.taint_level(),
Some(crate::taint::TaintLevel::Private)
);
assert_eq!(region.taint.as_ref().unwrap().entry_count(), 3);
let removed = region.remove_oldest().unwrap();
assert_eq!(removed.content, "assistant");
assert_eq!(region.entry_count(), 1);
assert_eq!(region.taint.as_ref().unwrap().entry_count(), 1);
assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
}
#[test]
fn test_sliding_window_evicts_entire_turn_group() {
let mut region = Region::new(
"conv".to_string(),
RegionKind::SlidingWindow {
max_items: 3,
eviction_strategy: EvictionStrategy::PerItem,
},
50000,
);
region
.add_typed_entry(
"assistant".to_string(),
10,
EntryKind::AssistantTurn {
tool_calls: vec![
SerializedToolCall {
id: "tc_1".to_string(),
name: "t1".to_string(),
arguments: serde_json::json!({}),
thought_signature: None,
},
SerializedToolCall {
id: "tc_2".to_string(),
name: "t2".to_string(),
arguments: serde_json::json!({}),
thought_signature: None,
},
],
},
)
.unwrap();
region
.add_typed_entry(
"r1".to_string(),
5,
EntryKind::ToolResult {
tool_call_id: "tc_1".to_string(),
tool_name: "t1".to_string(),
is_error: false,
},
)
.unwrap();
region
.add_typed_entry(
"r2".to_string(),
5,
EntryKind::ToolResult {
tool_call_id: "tc_2".to_string(),
tool_name: "t2".to_string(),
is_error: false,
},
)
.unwrap();
assert_eq!(region.entry_count(), 3);
region
.add_typed_entry("user msg".to_string(), 15, EntryKind::UserMessage)
.unwrap();
assert_eq!(region.entry_count(), 1);
assert_eq!(region.content[0].content, "user msg");
assert_eq!(region.current_tokens, 15);
}
#[test]
fn test_add_entry_with_metadata_tracks_taint_as_public() {
let mut region =
Region::new("data".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
region
.add_entry_with_metadata("content".to_string(), 10, serde_json::json!({"key": "val"}))
.unwrap();
assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
assert_eq!(region.taint.as_ref().unwrap().entry_count(), 1);
assert_eq!(
region.taint.as_ref().unwrap().entry_taint(0),
Some(crate::taint::TaintLevel::Public)
);
}
#[test]
fn test_add_typed_entry_tracks_taint_as_public() {
let mut region =
Region::new("conv".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
region
.add_typed_entry(
"assistant response".to_string(),
10,
EntryKind::AssistantTurn { tool_calls: vec![] },
)
.unwrap();
assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
assert_eq!(region.taint.as_ref().unwrap().entry_count(), 1);
assert_eq!(
region.taint.as_ref().unwrap().entry_taint(0),
Some(crate::taint::TaintLevel::Public)
);
}
#[test]
fn test_per_item_strategy_evicts_one_at_a_time() {
let mut region = Region::new(
"conv".to_string(),
RegionKind::SlidingWindow {
max_items: 3,
eviction_strategy: EvictionStrategy::PerItem,
},
50000,
);
for i in 0..5 {
region.add_entry(format!("msg{}", i), 10).unwrap();
}
assert_eq!(region.entry_count(), 3);
assert_eq!(region.content[0].content, "msg2");
assert_eq!(region.content[1].content, "msg3");
assert_eq!(region.content[2].content, "msg4");
}
#[test]
fn test_bulk_eviction_triggers_on_overflow() {
let mut region = Region::new(
"conv".to_string(),
RegionKind::SlidingWindow {
max_items: 5,
eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
},
50000,
);
for i in 0..8 {
region.add_entry(format!("msg{}", i), 10).unwrap();
}
assert_eq!(region.entry_count(), 8);
region.add_entry("msg8".to_string(), 10).unwrap();
assert_eq!(region.entry_count(), 5);
assert_eq!(region.content[0].content, "msg4");
}
#[test]
fn test_bulk_eviction_respects_turn_groups() {
let mut region = Region::new(
"conv".to_string(),
RegionKind::SlidingWindow {
max_items: 3,
eviction_strategy: EvictionStrategy::Bulk { overflow: 2 },
},
50000,
);
region
.add_typed_entry(
"assistant".to_string(),
10,
EntryKind::AssistantTurn {
tool_calls: vec![SerializedToolCall {
id: "tc1".to_string(),
name: "tool".to_string(),
arguments: serde_json::json!({}),
thought_signature: None,
}],
},
)
.unwrap();
region
.add_typed_entry(
"result".to_string(),
5,
EntryKind::ToolResult {
tool_call_id: "tc1".to_string(),
tool_name: "tool".to_string(),
is_error: false,
},
)
.unwrap();
region.add_entry("msg2".to_string(), 10).unwrap();
region.add_entry("msg3".to_string(), 10).unwrap();
region.add_entry("msg4".to_string(), 10).unwrap();
assert_eq!(region.entry_count(), 5);
region.add_entry("msg5".to_string(), 10).unwrap();
assert_eq!(region.entry_count(), 3);
assert_eq!(region.content[0].content, "msg3");
}
#[test]
fn test_bulk_eviction_under_overflow_no_eviction() {
let mut region = Region::new(
"conv".to_string(),
RegionKind::SlidingWindow {
max_items: 5,
eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
},
50000,
);
for i in 0..7 {
region.add_entry(format!("msg{}", i), 10).unwrap();
}
assert_eq!(region.entry_count(), 7);
}
#[test]
fn test_compact_sets_needs_message_compaction_flag() {
let mut region = Region::new(
"conv".to_string(),
RegionKind::SlidingWindow {
max_items: 5,
eviction_strategy: EvictionStrategy::Compact { compact_count: 3 },
},
50000,
);
assert!(!region.needs_message_compaction);
for i in 0..9 {
region.add_entry(format!("msg{}", i), 10).unwrap();
}
assert!(region.needs_message_compaction);
assert_eq!(region.entry_count(), 9);
}
#[test]
fn test_compact_fallback_to_bulk_eviction() {
let mut region = Region::new(
"conv".to_string(),
RegionKind::SlidingWindow {
max_items: 5,
eviction_strategy: EvictionStrategy::Compact { compact_count: 3 },
},
50000,
);
for i in 0..12 {
region.add_entry(format!("msg{}", i), 10).unwrap();
}
assert_eq!(region.entry_count(), 5);
assert_eq!(region.content[0].content, "msg7");
assert!(!region.needs_message_compaction);
}
#[test]
fn test_eviction_strategy_default_is_per_item() {
assert_eq!(EvictionStrategy::default(), EvictionStrategy::PerItem);
}
#[test]
fn test_remove_entries_by_prefix() {
let mut region = Region::new("system".to_string(), RegionKind::Pinned, 50000);
region
.add_entry("[Stage instructions: Be terse.]".to_string(), 10)
.unwrap();
region
.add_entry("Core identity block".to_string(), 20)
.unwrap();
region
.add_entry("[Stage instructions: Be verbose.]".to_string(), 15)
.unwrap();
assert_eq!(region.entry_count(), 3);
region.remove_entries_by_prefix("[Stage instructions:");
assert_eq!(region.entry_count(), 1);
assert_eq!(region.content[0].content, "Core identity block");
assert_eq!(region.current_tokens, 20);
}
#[test]
fn test_remove_entries_by_prefix_with_taint_tracking() {
let mut region =
Region::new("system".to_string(), RegionKind::Pinned, 50000).with_taint_tracking();
region
.add_tainted_entry(
"[Stage instructions: Be terse.]".to_string(),
10,
crate::taint::TaintLevel::Private,
)
.unwrap();
region
.add_tainted_entry(
"Core identity block".to_string(),
20,
crate::taint::TaintLevel::Public,
)
.unwrap();
region
.add_tainted_entry(
"[Stage instructions: Be verbose.]".to_string(),
15,
crate::taint::TaintLevel::Internal,
)
.unwrap();
assert_eq!(region.entry_count(), 3);
assert_eq!(
region.taint_level(),
Some(crate::taint::TaintLevel::Private)
);
region.remove_entries_by_prefix("[Stage instructions:");
assert_eq!(region.entry_count(), 1);
assert_eq!(region.content[0].content, "Core identity block");
assert_eq!(region.current_tokens, 20);
assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
assert_eq!(region.taint.as_ref().unwrap().entry_count(), 1);
}
#[test]
fn test_compact_below_threshold_no_flag() {
let mut region = Region::new(
"conv".to_string(),
RegionKind::SlidingWindow {
max_items: 5,
eviction_strategy: EvictionStrategy::Compact { compact_count: 3 },
},
50000,
);
for i in 0..8 {
region.add_entry(format!("msg{}", i), 10).unwrap();
}
assert!(!region.needs_message_compaction);
assert_eq!(region.entry_count(), 8);
}
#[test]
fn test_bulk_eviction_with_taint_tracking() {
let mut region = Region::new(
"conv".to_string(),
RegionKind::SlidingWindow {
max_items: 3,
eviction_strategy: EvictionStrategy::Bulk { overflow: 2 },
},
50000,
)
.with_taint_tracking();
region
.add_tainted_entry("private".to_string(), 10, crate::taint::TaintLevel::Private)
.unwrap();
for i in 1..5 {
region
.add_tainted_entry(format!("pub{}", i), 10, crate::taint::TaintLevel::Public)
.unwrap();
}
assert_eq!(region.entry_count(), 5);
region
.add_tainted_entry("pub5".to_string(), 10, crate::taint::TaintLevel::Public)
.unwrap();
assert_eq!(region.entry_count(), 3);
assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
}
#[test]
fn test_eviction_strategy_serde_roundtrip() {
let bulk = EvictionStrategy::Bulk { overflow: 5 };
let json = serde_json::to_string(&bulk).unwrap();
let parsed: EvictionStrategy = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, bulk);
let compact = EvictionStrategy::Compact { compact_count: 10 };
let json = serde_json::to_string(&compact).unwrap();
let parsed: EvictionStrategy = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, compact);
let per_item = EvictionStrategy::PerItem;
let json = serde_json::to_string(&per_item).unwrap();
let parsed: EvictionStrategy = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, per_item);
}
#[test]
fn test_sliding_window_kind_equality_with_eviction_strategy() {
assert_eq!(
RegionKind::SlidingWindow {
max_items: 10,
eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
},
RegionKind::SlidingWindow {
max_items: 10,
eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
}
);
assert_ne!(
RegionKind::SlidingWindow {
max_items: 10,
eviction_strategy: EvictionStrategy::PerItem,
},
RegionKind::SlidingWindow {
max_items: 10,
eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
}
);
}
#[test]
fn test_needs_message_compaction_default_false() {
let region = Region::new("conv".to_string(), RegionKind::Temporary, 1000);
assert!(!region.needs_message_compaction);
}
#[test]
fn test_add_typed_entry_validates_schema() {
let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000)
.with_schema(RegionSchema::new(ContentFormat::Json));
let result = region.add_typed_entry("not json".to_string(), 5, EntryKind::Text);
assert!(result.is_err());
assert_eq!(region.entry_count(), 0);
}
#[test]
fn test_add_typed_entry_checks_budget() {
let mut region = Region::new("data".to_string(), RegionKind::Temporary, 10);
let result = region.add_typed_entry("too big".to_string(), 20, EntryKind::UserMessage);
assert!(result.is_err());
assert_eq!(region.entry_count(), 0);
}
#[test]
fn test_add_tainted_entry_without_taint_tracking() {
let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
region
.add_tainted_entry("data".to_string(), 10, crate::taint::TaintLevel::Private)
.unwrap();
assert_eq!(region.entry_count(), 1);
assert_eq!(region.taint_level(), None);
}
#[test]
fn test_remove_entries_by_prefix_no_match() {
let mut region = Region::new("system".to_string(), RegionKind::Pinned, 50000);
region.add_entry("Keep this".to_string(), 10).unwrap();
region.add_entry("And this".to_string(), 20).unwrap();
region.remove_entries_by_prefix("[Stage instructions:");
assert_eq!(region.entry_count(), 2);
assert_eq!(region.current_tokens, 30);
}
#[test]
fn test_hashmap_region_upsert_and_get() {
let mut region = Region::new(
"files".to_string(),
RegionKind::HashMap { max_entries: None },
10000,
);
region
.upsert_by_key("src/main.rs", "fn main() {}".to_string(), 10)
.unwrap();
region
.upsert_by_key("src/lib.rs", "pub mod foo;".to_string(), 8)
.unwrap();
assert_eq!(region.entry_count(), 2);
assert_eq!(region.current_tokens, 18);
let entry = region.get_by_key("src/main.rs").unwrap();
assert_eq!(entry.content, "fn main() {}");
assert_eq!(entry.key.as_deref(), Some("src/main.rs"));
}
#[test]
fn test_hashmap_region_upsert_replaces_existing() {
let mut region = Region::new(
"files".to_string(),
RegionKind::HashMap { max_entries: None },
10000,
);
region
.upsert_by_key("file.rs", "version 1".to_string(), 10)
.unwrap();
assert_eq!(region.current_tokens, 10);
region
.upsert_by_key("file.rs", "version 2".to_string(), 15)
.unwrap();
assert_eq!(region.entry_count(), 1);
assert_eq!(region.current_tokens, 15);
assert_eq!(region.get_by_key("file.rs").unwrap().content, "version 2");
}
#[test]
fn test_hashmap_region_remove_by_key() {
let mut region = Region::new(
"files".to_string(),
RegionKind::HashMap { max_entries: None },
10000,
);
region.upsert_by_key("a.rs", "aaa".to_string(), 10).unwrap();
region.upsert_by_key("b.rs", "bbb".to_string(), 20).unwrap();
assert!(region.remove_by_key("a.rs"));
assert_eq!(region.entry_count(), 1);
assert_eq!(region.current_tokens, 20);
assert!(region.get_by_key("a.rs").is_none());
assert!(!region.remove_by_key("nonexistent"));
}
#[test]
fn test_hashmap_region_keys() {
let mut region = Region::new(
"files".to_string(),
RegionKind::HashMap { max_entries: None },
10000,
);
region.upsert_by_key("x.rs", "x".to_string(), 5).unwrap();
region.upsert_by_key("y.rs", "y".to_string(), 5).unwrap();
let keys = region.keys();
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"x.rs"));
assert!(keys.contains(&"y.rs"));
}
#[test]
fn test_hashmap_region_lru_eviction_on_max_tokens() {
let mut region = Region::new(
"files".to_string(),
RegionKind::HashMap { max_entries: None },
30, );
region.upsert_by_key("a.rs", "aaa".to_string(), 10).unwrap();
region.content[0].timestamp -= 100;
region.upsert_by_key("b.rs", "bbb".to_string(), 10).unwrap();
region.upsert_by_key("c.rs", "ccc".to_string(), 10).unwrap();
assert_eq!(region.entry_count(), 3);
assert_eq!(region.current_tokens, 30);
region.upsert_by_key("d.rs", "ddd".to_string(), 10).unwrap();
assert_eq!(region.entry_count(), 3);
assert!(region.get_by_key("a.rs").is_none());
assert!(region.get_by_key("d.rs").is_some());
}
#[test]
fn test_hashmap_region_max_entries_eviction() {
let mut region = Region::new(
"files".to_string(),
RegionKind::HashMap {
max_entries: Some(2),
},
10000,
);
region.upsert_by_key("a.rs", "aaa".to_string(), 10).unwrap();
region.content[0].timestamp -= 100; region.upsert_by_key("b.rs", "bbb".to_string(), 10).unwrap();
assert_eq!(region.entry_count(), 2);
region.upsert_by_key("c.rs", "ccc".to_string(), 10).unwrap();
assert_eq!(region.entry_count(), 2);
assert!(region.get_by_key("a.rs").is_none());
assert!(region.get_by_key("c.rs").is_some());
}
#[test]
fn test_hashmap_region_upsert_too_large_for_budget() {
let mut region = Region::new(
"files".to_string(),
RegionKind::HashMap { max_entries: None },
5, );
let result = region.upsert_by_key("big.rs", "huge content".to_string(), 100);
assert!(result.is_err());
}
#[test]
fn test_hashmap_region_kind_equality() {
assert_eq!(
RegionKind::HashMap {
max_entries: Some(10)
},
RegionKind::HashMap {
max_entries: Some(10)
}
);
assert_ne!(
RegionKind::HashMap {
max_entries: Some(10)
},
RegionKind::HashMap {
max_entries: Some(20)
}
);
assert_ne!(
RegionKind::HashMap { max_entries: None },
RegionKind::Pinned
);
}
#[test]
fn test_hashmap_cache_hint() {
let kind = RegionKind::HashMap { max_entries: None };
assert_eq!(kind.cache_hint(), crate::cache::CacheHint::UntilChanged);
}
#[test]
fn test_region_entry_key_default_none() {
let mut region = Region::new("test".to_string(), RegionKind::Temporary, 1000);
region.add_entry("content".to_string(), 10).unwrap();
assert!(region.content[0].key.is_none());
}
#[test]
fn test_region_entry_key_serde_skip_when_none() {
let entry = RegionEntry {
content: "test".to_string(),
tokens: 5,
timestamp: 0,
metadata: None,
kind: EntryKind::default(),
key: None,
};
let json = serde_json::to_string(&entry).unwrap();
assert!(!json.contains("key"));
}
#[test]
fn test_region_entry_key_serde_roundtrip() {
let entry = RegionEntry {
content: "test".to_string(),
tokens: 5,
timestamp: 0,
metadata: None,
kind: EntryKind::default(),
key: Some("mykey".to_string()),
};
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("mykey"));
let back: RegionEntry = serde_json::from_str(&json).unwrap();
assert_eq!(back.key.as_deref(), Some("mykey"));
}
#[test]
fn test_hashmap_region_creation_and_basic_properties() {
let region = Region::new(
"lookup".to_string(),
RegionKind::HashMap {
max_entries: Some(5),
},
2000,
);
assert_eq!(region.name, "lookup");
assert_eq!(
region.kind,
RegionKind::HashMap {
max_entries: Some(5)
}
);
assert_eq!(region.max_tokens, 2000);
assert_eq!(region.current_tokens, 0);
assert_eq!(region.entry_count(), 0);
assert!(region.content.is_empty());
}
#[test]
fn test_hashmap_upsert_insert_new_entry() {
let mut region = Region::new(
"store".to_string(),
RegionKind::HashMap {
max_entries: Some(5),
},
5000,
);
region
.upsert_by_key("config.toml", "[package]\nname = \"foo\"".to_string(), 12)
.unwrap();
assert_eq!(region.entry_count(), 1);
assert_eq!(region.current_tokens, 12);
let entry = region.get_by_key("config.toml").unwrap();
assert_eq!(entry.content, "[package]\nname = \"foo\"");
assert_eq!(entry.tokens, 12);
assert_eq!(entry.key.as_deref(), Some("config.toml"));
}
#[test]
fn test_hashmap_upsert_update_existing_entry() {
let mut region = Region::new(
"store".to_string(),
RegionKind::HashMap { max_entries: None },
5000,
);
region
.upsert_by_key("readme.md", "# Old".to_string(), 20)
.unwrap();
assert_eq!(region.current_tokens, 20);
region
.upsert_by_key("readme.md", "# New and improved".to_string(), 35)
.unwrap();
assert_eq!(region.entry_count(), 1);
assert_eq!(region.current_tokens, 35);
let entry = region.get_by_key("readme.md").unwrap();
assert_eq!(entry.content, "# New and improved");
assert_eq!(entry.tokens, 35);
}
#[test]
fn test_hashmap_upsert_lru_eviction_on_max_tokens() {
let mut region = Region::new(
"files".to_string(),
RegionKind::HashMap { max_entries: None },
100, );
region
.upsert_by_key("first.rs", "first content".to_string(), 40)
.unwrap();
region.content[0].timestamp -= 200;
region
.upsert_by_key("second.rs", "second content".to_string(), 40)
.unwrap();
region.content[1].timestamp -= 100;
region
.upsert_by_key("third.rs", "third content".to_string(), 20)
.unwrap();
region
.upsert_by_key("fourth.rs", "fourth content".to_string(), 30)
.unwrap();
assert!(region.get_by_key("first.rs").is_none());
assert!(region.get_by_key("fourth.rs").is_some());
assert!(region.current_tokens <= 100);
}
#[test]
fn test_hashmap_upsert_max_entries_enforcement() {
let mut region = Region::new(
"cache".to_string(),
RegionKind::HashMap {
max_entries: Some(2),
},
50000,
);
region
.upsert_by_key("alpha", "aaa".to_string(), 10)
.unwrap();
region.content[0].timestamp -= 200;
region.upsert_by_key("beta", "bbb".to_string(), 10).unwrap();
region.content[1].timestamp -= 100;
region
.upsert_by_key("gamma", "ccc".to_string(), 10)
.unwrap();
assert_eq!(region.entry_count(), 2);
assert!(region.get_by_key("alpha").is_none());
assert!(region.get_by_key("beta").is_some());
assert!(region.get_by_key("gamma").is_some());
}
#[test]
fn test_hashmap_get_by_key_found_and_not_found() {
let mut region = Region::new(
"data".to_string(),
RegionKind::HashMap { max_entries: None },
5000,
);
region
.upsert_by_key("exists", "hello".to_string(), 5)
.unwrap();
let found = region.get_by_key("exists");
assert!(found.is_some());
assert_eq!(found.unwrap().content, "hello");
let missing = region.get_by_key("does_not_exist");
assert!(missing.is_none());
}
#[test]
fn test_hashmap_remove_by_key_exists() {
let mut region = Region::new(
"data".to_string(),
RegionKind::HashMap { max_entries: None },
5000,
);
region
.upsert_by_key("target", "remove me".to_string(), 25)
.unwrap();
assert_eq!(region.current_tokens, 25);
let removed = region.remove_by_key("target");
assert!(removed);
assert_eq!(region.entry_count(), 0);
assert_eq!(region.current_tokens, 0);
assert!(region.get_by_key("target").is_none());
}
#[test]
fn test_hashmap_remove_by_key_does_not_exist() {
let mut region = Region::new(
"data".to_string(),
RegionKind::HashMap { max_entries: None },
5000,
);
let removed = region.remove_by_key("ghost");
assert!(!removed);
}
#[test]
fn test_hashmap_keys_empty_populated_after_removal() {
let mut region = Region::new(
"data".to_string(),
RegionKind::HashMap { max_entries: None },
5000,
);
assert!(region.keys().is_empty());
region.upsert_by_key("one", "1".to_string(), 5).unwrap();
region.upsert_by_key("two", "2".to_string(), 5).unwrap();
region.upsert_by_key("three", "3".to_string(), 5).unwrap();
let keys = region.keys();
assert_eq!(keys.len(), 3);
assert!(keys.contains(&"one"));
assert!(keys.contains(&"two"));
assert!(keys.contains(&"three"));
region.remove_by_key("two");
let keys = region.keys();
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"one"));
assert!(!keys.contains(&"two"));
assert!(keys.contains(&"three"));
}
#[test]
fn test_region_entry_serialization_with_key_field() {
let entry_with_key = RegionEntry {
content: "some data".to_string(),
tokens: 10,
timestamp: 1234567890,
metadata: None,
kind: EntryKind::default(),
key: Some("mykey".to_string()),
};
let json = serde_json::to_string(&entry_with_key).unwrap();
let deserialized: RegionEntry = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.key.as_deref(), Some("mykey"));
assert_eq!(deserialized.content, "some data");
assert_eq!(deserialized.tokens, 10);
let entry_no_key = RegionEntry {
content: "no key data".to_string(),
tokens: 7,
timestamp: 1234567890,
metadata: None,
kind: EntryKind::default(),
key: None,
};
let json = serde_json::to_string(&entry_no_key).unwrap();
assert!(!json.contains("\"key\""));
let deserialized: RegionEntry = serde_json::from_str(&json).unwrap();
assert!(deserialized.key.is_none());
assert_eq!(deserialized.content, "no key data");
}
#[test]
fn test_hashmap_partial_eq() {
let a = RegionKind::HashMap {
max_entries: Some(5),
};
let b = RegionKind::HashMap {
max_entries: Some(5),
};
let c = RegionKind::HashMap {
max_entries: Some(10),
};
let d = RegionKind::HashMap { max_entries: None };
assert_eq!(a, b);
assert_ne!(a, c);
assert_ne!(a, d);
assert_ne!(c, d);
assert_ne!(a, RegionKind::Pinned);
assert_ne!(a, RegionKind::Temporary);
}
#[test]
fn test_hashmap_cache_hint_returns_until_changed() {
let kind = RegionKind::HashMap { max_entries: None };
assert_eq!(kind.cache_hint(), crate::cache::CacheHint::UntilChanged);
let kind_with_max = RegionKind::HashMap {
max_entries: Some(10),
};
assert_eq!(
kind_with_max.cache_hint(),
crate::cache::CacheHint::UntilChanged
);
}
#[test]
fn test_remove_by_key_recomputes_taint_when_tracking_enabled() {
let mut region = Region::new(
"kv".to_string(),
RegionKind::HashMap { max_entries: None },
10_000,
)
.with_taint_tracking();
region
.upsert_by_key("k1", "value one".to_string(), 10)
.unwrap();
region
.upsert_by_key("k2", "value two".to_string(), 10)
.unwrap();
assert!(region.remove_by_key("k1"));
assert!(!region.remove_by_key("missing"));
assert_eq!(region.entry_count(), 1);
assert_eq!(region.current_tokens, 10);
}
#[test]
fn test_evict_lru_entry_runs_taint_fixup() {
let mut region = Region::new(
"kv".to_string(),
RegionKind::HashMap {
max_entries: Some(1),
},
10_000,
)
.with_taint_tracking();
region
.upsert_by_key("first", "aaa".to_string(), 10)
.unwrap();
region
.upsert_by_key("second", "bbb".to_string(), 10)
.unwrap();
assert_eq!(region.entry_count(), 1);
assert!(region.get_by_key("second").is_some());
assert!(region.get_by_key("first").is_none());
}
#[test]
fn test_evict_lru_entry_on_empty_region_is_noop() {
let mut region = Region::new(
"kv".to_string(),
RegionKind::HashMap {
max_entries: Some(4),
},
1000,
);
assert_eq!(region.entry_count(), 0);
region.evict_lru_entry();
assert_eq!(region.entry_count(), 0);
assert_eq!(region.current_tokens, 0);
}
}