1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
16#[serde(tag = "type")]
17pub enum EntryKind {
18 #[default]
20 Text,
21 UserMessage,
23 AssistantTurn {
25 tool_calls: Vec<SerializedToolCall>,
29 },
30 ToolResult {
32 tool_call_id: String,
35 tool_name: String,
37 is_error: bool,
40 },
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
45pub struct SerializedToolCall {
46 pub id: String,
49 pub name: String,
51 pub arguments: serde_json::Value,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub thought_signature: Option<String>,
57}
58
59#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
66#[serde(tag = "strategy", rename_all = "snake_case")]
67pub enum EvictionStrategy {
68 #[default]
70 PerItem,
71 Bulk {
74 overflow: usize,
77 },
78 Compact {
82 compact_count: usize,
84 },
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
94pub enum RegionKind {
95 Pinned,
102
103 SlidingWindow {
110 max_items: usize,
112 eviction_strategy: EvictionStrategy,
114 },
115
116 Temporary,
122
123 Compacting {
129 threshold_tokens: usize,
131 },
132
133 Clearable,
140
141 CompactHistory {
148 source_region: String,
150 },
151
152 HashMap {
156 max_entries: Option<usize>,
158 },
159
160 Checklist,
170
171 Custom {
187 script: String,
189 persistent: bool,
192 },
193}
194
195impl PartialEq for RegionKind {
196 #[inline(never)]
197 fn eq(&self, other: &Self) -> bool {
198 match (self, other) {
199 (Self::Pinned, Self::Pinned)
200 | (Self::Temporary, Self::Temporary)
201 | (Self::Clearable, Self::Clearable) => true,
202 (
203 Self::SlidingWindow {
204 max_items: a,
205 eviction_strategy: sa,
206 },
207 Self::SlidingWindow {
208 max_items: b,
209 eviction_strategy: sb,
210 },
211 ) => a == b && sa == sb,
212 (
213 Self::Compacting {
214 threshold_tokens: a,
215 },
216 Self::Compacting {
217 threshold_tokens: b,
218 },
219 ) => a == b,
220 (
221 Self::CompactHistory { source_region: a },
222 Self::CompactHistory { source_region: b },
223 ) => a == b,
224 (Self::HashMap { max_entries: a }, Self::HashMap { max_entries: b }) => a == b,
225 (Self::Checklist, Self::Checklist) => true,
226 (
227 Self::Custom {
228 script: a,
229 persistent: pa,
230 },
231 Self::Custom {
232 script: b,
233 persistent: pb,
234 },
235 ) => a == b && pa == pb,
236 _ => false,
237 }
238 }
239}
240impl Eq for RegionKind {}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
249pub struct ChecklistItem {
250 pub id: usize,
252 pub text: String,
254 pub done: bool,
256 pub note: Option<String>,
258}
259
260const ITEM_ID: &str = "checklist_id";
262const ITEM_DONE: &str = "checklist_done";
264const ITEM_NOTE: &str = "checklist_note";
266
267impl RegionEntry {
268 pub fn as_checklist_item(&self) -> Option<ChecklistItem> {
270 let meta = self.metadata.as_ref()?;
271 Some(ChecklistItem {
272 id: meta.get(ITEM_ID)?.as_u64()? as usize,
273 text: self.content.clone(),
274 done: meta
275 .get(ITEM_DONE)
276 .and_then(|v| v.as_bool())
277 .unwrap_or(false),
278 note: meta
279 .get(ITEM_NOTE)
280 .and_then(|v| v.as_str())
281 .map(str::to_string),
282 })
283 }
284}
285
286impl Region {
287 pub fn checklist_items(&self) -> Vec<ChecklistItem> {
289 self.content
290 .iter()
291 .filter_map(RegionEntry::as_checklist_item)
292 .collect()
293 }
294
295 pub fn open_checklist_items(&self) -> Vec<ChecklistItem> {
297 self.checklist_items()
298 .into_iter()
299 .filter(|i| !i.done)
300 .collect()
301 }
302
303 pub fn add_checklist_item(
310 &mut self,
311 text: String,
312 tokens: usize,
313 ) -> crate::error::Result<usize> {
314 let id = self
315 .checklist_items()
316 .iter()
317 .map(|i| i.id)
318 .max()
319 .unwrap_or(0)
320 + 1;
321 self.add_entry_with_metadata(
322 text,
323 tokens,
324 serde_json::json!({ ITEM_ID: id, ITEM_DONE: false }),
325 )?;
326 Ok(id)
327 }
328
329 pub fn complete_checklist_item(&mut self, id: usize) -> bool {
331 self.set_item_field(id, ITEM_DONE, serde_json::Value::Bool(true))
332 }
333
334 pub fn note_checklist_item(&mut self, id: usize, note: &str) -> bool {
336 self.set_item_field(id, ITEM_NOTE, serde_json::Value::String(note.to_string()))
337 }
338
339 fn set_item_field(&mut self, id: usize, key: &str, value: serde_json::Value) -> bool {
341 for entry in &mut self.content {
342 let is_target = entry
343 .metadata
344 .as_ref()
345 .and_then(|m| m.get(ITEM_ID))
346 .and_then(serde_json::Value::as_u64)
347 .is_some_and(|found| found as usize == id);
348 if is_target && let Some(serde_json::Value::Object(meta)) = entry.metadata.as_mut() {
349 meta.insert(key.to_string(), value);
350 return true;
351 }
352 }
353 false
354 }
355
356 pub fn render_checklist(&self) -> String {
362 let items = self.checklist_items();
363 if items.is_empty() {
364 return String::new();
365 }
366 let (open, done): (Vec<_>, Vec<_>) = items.into_iter().partition(|i| !i.done);
367 let mut out = String::new();
368 for item in open.iter().chain(done.iter()) {
369 let box_ = match item.done {
370 true => "[x]",
371 false => "[ ]",
372 };
373 out.push_str(&format!("{box_} {} {}", item.id, item.text));
374 if let Some(note) = &item.note {
375 out.push_str(&format!("\n note: {note}"));
376 }
377 out.push('\n');
378 }
379 format!(
380 "Checklist ({} open, {} done):\n{}",
381 open.len(),
382 done.len(),
383 out.trim_end()
384 )
385 }
386}
387
388impl RegionKind {
389 pub fn cache_hint(&self) -> crate::cache::CacheHint {
391 match self {
392 RegionKind::Pinned | RegionKind::CompactHistory { .. } => {
393 crate::cache::CacheHint::Always
394 }
395 RegionKind::Compacting { .. } => crate::cache::CacheHint::UntilChanged,
396 RegionKind::SlidingWindow { .. } => crate::cache::CacheHint::SlidingPrefix {
397 stable_fraction: 0.75,
398 },
399 RegionKind::HashMap { .. } => crate::cache::CacheHint::UntilChanged,
400 RegionKind::Checklist => crate::cache::CacheHint::UntilChanged,
403 RegionKind::Temporary | RegionKind::Clearable => crate::cache::CacheHint::Never,
404 RegionKind::Custom { persistent, .. } => {
408 if *persistent {
409 crate::cache::CacheHint::Always
410 } else {
411 crate::cache::CacheHint::UntilChanged
412 }
413 }
414 }
415 }
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct Region {
424 pub name: String,
426
427 pub kind: RegionKind,
429
430 pub content: Vec<RegionEntry>,
432
433 pub max_tokens: usize,
435
436 pub current_tokens: usize,
438
439 pub schema: Option<RegionSchema>,
441
442 #[serde(default, skip_serializing_if = "Option::is_none")]
444 pub taint: Option<crate::taint::RegionTaint>,
445
446 #[serde(default)]
450 pub needs_message_compaction: bool,
451
452 #[serde(default = "crate::region::default_true")]
458 pub summarizable: bool,
459
460 #[serde(default)]
462 pub admission: Admission,
463}
464
465#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
478#[serde(rename_all = "snake_case")]
479pub enum Admission {
480 #[default]
483 Evict,
484 Reject,
487}
488
489mod schema;
490
491pub use schema::{ContentFormat, RegionSchema, Validator};
492
493pub(crate) fn default_true() -> bool {
495 true
496}
497
498impl Region {
499 pub fn new(name: String, kind: RegionKind, max_tokens: usize) -> Self {
501 Self {
502 name,
503 kind,
504 content: Vec::new(),
505 max_tokens,
506 current_tokens: 0,
507 schema: None,
508 taint: None,
509 needs_message_compaction: false,
510 summarizable: true,
511 admission: Admission::default(),
512 }
513 }
514
515 pub fn with_taint_tracking(mut self) -> Self {
517 self.taint = Some(crate::taint::RegionTaint::new());
518 self
519 }
520
521 pub fn enable_taint_tracking(&mut self) {
523 if self.taint.is_none() {
524 self.taint = Some(crate::taint::RegionTaint::new());
525 }
526 }
527
528 pub fn taint_level(&self) -> Option<crate::taint::TaintLevel> {
530 self.taint.as_ref().map(|t| t.level())
531 }
532
533 fn push_entry(
546 &mut self,
547 content: String,
548 tokens: usize,
549 metadata: Option<serde_json::Value>,
550 kind: EntryKind,
551 taint_level: crate::taint::TaintLevel,
552 key: Option<&str>,
553 ) -> crate::error::Result<()> {
554 if let Some(schema) = &self.schema {
555 schema.validate(&content)?;
556 }
557
558 if self.current_tokens + tokens > self.max_tokens {
559 if self.admission == Admission::Reject && !self.content.is_empty() {
565 return Err(crate::error::Error::RegionFull {
566 region: self.name.clone(),
567 used: self.current_tokens,
568 max: self.max_tokens,
569 });
570 }
571 return Err(crate::error::Error::TokenBudgetExceeded {
572 used: self.current_tokens + tokens,
573 max: self.max_tokens,
574 });
575 }
576 if self.admission == Admission::Reject && self.would_roll_off() {
580 return Err(crate::error::Error::RegionFull {
581 region: self.name.clone(),
582 used: self.current_tokens,
583 max: self.max_tokens,
584 });
585 }
586
587 self.content.push(RegionEntry {
588 content,
589 tokens,
590 timestamp: chrono::Utc::now().timestamp(),
591 metadata,
592 kind,
593 key: key.map(str::to_string),
594 });
595 self.current_tokens += tokens;
596
597 if let Some(taint) = &mut self.taint {
601 taint.add_entry(taint_level);
602 }
603
604 self.enforce_sliding_window();
605
606 Ok(())
607 }
608
609 fn would_roll_off(&self) -> bool {
614 match &self.kind {
615 RegionKind::SlidingWindow { max_items, .. } => self.content.len() + 1 > *max_items,
616 _ => false,
617 }
618 }
619
620 pub fn add_keyed_entry(
626 &mut self,
627 key: &str,
628 content: String,
629 tokens: usize,
630 ) -> crate::error::Result<()> {
631 self.push_entry(
632 content,
633 tokens,
634 None,
635 EntryKind::default(),
636 crate::taint::TaintLevel::Public,
637 Some(key),
638 )
639 }
640
641 pub fn remove_at(&mut self, index: usize) -> bool {
647 if index >= self.content.len() {
648 return false;
649 }
650 let entry = self.content.remove(index);
651 self.current_tokens = self.current_tokens.saturating_sub(entry.tokens);
652 true
653 }
654
655 pub fn release_oldest(&mut self, n: usize) -> usize {
660 let count = n.min(self.content.len());
661 for _ in 0..count {
662 self.remove_oldest();
663 }
664 count
665 }
666
667 pub fn add_tainted_entry(
669 &mut self,
670 content: String,
671 tokens: usize,
672 taint_level: crate::taint::TaintLevel,
673 ) -> crate::error::Result<()> {
674 self.push_entry(
675 content,
676 tokens,
677 None,
678 EntryKind::default(),
679 taint_level,
680 None,
681 )
682 }
683
684 pub fn add_typed_tainted_entry(
693 &mut self,
694 content: String,
695 tokens: usize,
696 kind: EntryKind,
697 taint_level: crate::taint::TaintLevel,
698 ) -> crate::error::Result<()> {
699 self.push_entry(content, tokens, None, kind, taint_level, None)
700 }
701
702 pub fn with_schema(mut self, schema: RegionSchema) -> Self {
704 self.schema = Some(schema);
705 self
706 }
707
708 pub fn add_entry(&mut self, content: String, tokens: usize) -> crate::error::Result<()> {
713 self.push_entry(
714 content,
715 tokens,
716 None,
717 EntryKind::default(),
718 crate::taint::TaintLevel::Public,
719 None,
720 )
721 }
722
723 pub fn add_entry_with_metadata(
725 &mut self,
726 content: String,
727 tokens: usize,
728 metadata: serde_json::Value,
729 ) -> crate::error::Result<()> {
730 self.push_entry(
731 content,
732 tokens,
733 Some(metadata),
734 EntryKind::default(),
735 crate::taint::TaintLevel::Public,
736 None,
737 )
738 }
739
740 pub fn add_typed_entry(
746 &mut self,
747 content: String,
748 tokens: usize,
749 kind: EntryKind,
750 ) -> crate::error::Result<()> {
751 self.push_entry(
752 content,
753 tokens,
754 None,
755 kind,
756 crate::taint::TaintLevel::Public,
757 None,
758 )
759 }
760
761 pub fn carry_entry(&mut self, entry: RegionEntry) -> crate::error::Result<()> {
775 if self.current_tokens + entry.tokens > self.max_tokens {
777 return Err(crate::error::Error::TokenBudgetExceeded {
778 used: self.current_tokens + entry.tokens,
779 max: self.max_tokens,
780 });
781 }
782
783 self.current_tokens += entry.tokens;
784 self.content.push(entry);
785
786 self.enforce_sliding_window();
788
789 Ok(())
790 }
791
792 pub fn upsert_by_key(
795 &mut self,
796 key: &str,
797 content: String,
798 tokens: usize,
799 ) -> Result<(), String> {
800 if let Some(pos) = self
802 .content
803 .iter()
804 .position(|e| e.key.as_deref() == Some(key))
805 {
806 let old_tokens = self.content[pos].tokens;
807 self.current_tokens -= old_tokens;
808 self.content[pos].content = content;
809 self.content[pos].tokens = tokens;
810 self.content[pos].timestamp = chrono::Utc::now().timestamp();
811 self.current_tokens += tokens;
812 return Ok(());
813 }
814
815 let max_entries = if let RegionKind::HashMap {
817 max_entries: Some(max),
818 } = &self.kind
819 {
820 Some(*max)
821 } else {
822 None
823 };
824 if let Some(max) = max_entries {
825 while self.content.len() >= max {
826 self.evict_lru_entry();
827 }
828 }
829
830 while self.current_tokens + tokens > self.max_tokens && !self.content.is_empty() {
832 self.evict_lru_entry();
833 }
834
835 if self.current_tokens + tokens > self.max_tokens {
836 return Err(format!(
837 "Entry ({} tokens) exceeds region budget ({} max)",
838 tokens, self.max_tokens
839 ));
840 }
841
842 self.content.push(RegionEntry {
843 content,
844 tokens,
845 timestamp: chrono::Utc::now().timestamp(),
846 metadata: None,
847 kind: EntryKind::default(),
848 key: Some(key.to_string()),
849 });
850 self.current_tokens += tokens;
851 Ok(())
852 }
853
854 pub fn get_by_key(&self, key: &str) -> Option<&RegionEntry> {
856 self.content.iter().find(|e| e.key.as_deref() == Some(key))
857 }
858
859 pub fn remove_by_key(&mut self, key: &str) -> bool {
861 if let Some(pos) = self
862 .content
863 .iter()
864 .position(|e| e.key.as_deref() == Some(key))
865 {
866 let tokens = self.content[pos].tokens;
867 self.content.remove(pos);
868 self.current_tokens -= tokens;
869 if let Some(taint) = &mut self.taint {
870 taint.remove_at(pos);
871 }
872 true
873 } else {
874 false
875 }
876 }
877
878 pub fn keys(&self) -> Vec<&str> {
880 self.content
881 .iter()
882 .filter_map(|e| e.key.as_deref())
883 .collect()
884 }
885
886 fn evict_lru_entry(&mut self) {
888 if self.content.is_empty() {
889 return;
890 }
891 let oldest_idx = self
892 .content
893 .iter()
894 .enumerate()
895 .min_by_key(|(_, e)| e.timestamp)
896 .map(|(i, _)| i)
897 .unwrap_or(0);
898 let tokens = self.content[oldest_idx].tokens;
899 self.content.remove(oldest_idx);
900 self.current_tokens -= tokens;
901 if let Some(taint) = &mut self.taint {
902 taint.remove_at(oldest_idx);
903 }
904 }
905
906 fn enforce_sliding_window(&mut self) {
917 if let RegionKind::SlidingWindow {
918 max_items,
919 eviction_strategy,
920 } = &self.kind
921 {
922 let max = *max_items;
923 match eviction_strategy.clone() {
924 EvictionStrategy::PerItem => {
925 while self.content.len() > max && self.remove_oldest().is_some() {}
929 }
930 EvictionStrategy::Bulk { overflow } => {
931 if self.content.len() > max + overflow {
932 while self.content.len() > max && self.remove_oldest().is_some() {}
933 }
934 }
935 EvictionStrategy::Compact { compact_count } => {
936 if self.content.len() > max + compact_count * 2 {
937 while self.content.len() > max && self.remove_oldest().is_some() {}
940 self.needs_message_compaction = false;
941 } else if self.content.len() > max + compact_count {
942 self.needs_message_compaction = true;
943 }
944 }
945 }
946 }
947 }
948
949 fn turn_group_size_at(&self, idx: usize) -> usize {
957 if idx >= self.content.len() {
958 return 0;
959 }
960 match &self.content[idx].kind {
961 EntryKind::AssistantTurn { .. } => {
962 let mut size = 1;
963 while idx + size < self.content.len() {
964 if matches!(self.content[idx + size].kind, EntryKind::ToolResult { .. }) {
965 size += 1;
966 } else {
967 break;
968 }
969 }
970 size
971 }
972 _ => 1,
973 }
974 }
975
976 pub fn clear(&mut self) {
978 self.content.clear();
979 self.current_tokens = 0;
980 if let Some(taint) = &mut self.taint {
981 taint.clear();
982 }
983 }
984
985 pub fn remove_oldest(&mut self) -> Option<RegionEntry> {
987 if self.content.is_empty() {
988 return None;
989 }
990 let group_size = self.turn_group_size_at(0);
994 let mut first = None;
995 let mut extra_tokens = 0usize;
996 let mut i = 0;
999 while i < group_size && !self.content.is_empty() {
1000 let entry_tokens = self.content[0].tokens;
1001 self.current_tokens -= entry_tokens;
1002 let removed = self.content.remove(0);
1003 if let Some(taint) = &mut self.taint {
1004 taint.remove_oldest();
1005 }
1006 if i == 0 {
1007 first = Some(removed);
1008 } else {
1009 extra_tokens += entry_tokens;
1010 }
1011 i += 1;
1012 }
1013 first.map(|mut entry| {
1019 entry.tokens += extra_tokens;
1020 entry
1021 })
1022 }
1023
1024 pub fn remove_entries_by_prefix(&mut self, prefix: &str) {
1030 let mut i = 0;
1031 while i < self.content.len() {
1032 if self.content[i].content.starts_with(prefix) {
1033 let tokens = self.content[i].tokens;
1034 self.content.remove(i);
1035 self.current_tokens -= tokens;
1036 if let Some(taint) = &mut self.taint {
1037 taint.remove_at(i);
1038 }
1039 } else {
1040 i += 1;
1041 }
1042 }
1043 }
1044
1045 pub fn entry_count(&self) -> usize {
1047 self.content.len()
1048 }
1049
1050 pub fn needs_compaction(&self) -> bool {
1052 if let RegionKind::Compacting { threshold_tokens } = self.kind {
1053 self.current_tokens > threshold_tokens
1054 } else {
1055 false
1056 }
1057 }
1058}
1059
1060#[derive(Debug, Clone, Serialize, Deserialize)]
1064pub struct RegionEntry {
1065 pub content: String,
1067
1068 pub tokens: usize,
1070
1071 pub timestamp: i64,
1073
1074 pub metadata: Option<serde_json::Value>,
1076
1077 #[serde(default)]
1081 pub kind: EntryKind,
1082
1083 #[serde(default, skip_serializing_if = "Option::is_none")]
1085 pub key: Option<String>,
1086}
1087
1088#[cfg(test)]
1091mod tests {
1092 use super::*;
1093
1094 fn checklist() -> Region {
1097 Region::new("todos".to_string(), RegionKind::Checklist, 10_000)
1098 }
1099
1100 #[test]
1106 fn a_malformed_entry_is_not_an_item() {
1107 let mut r = checklist();
1108 r.add_entry("a plain note".to_string(), 3).unwrap();
1110 r.add_entry_with_metadata(
1112 "something else".to_string(),
1113 3,
1114 serde_json::json!({ "unrelated": true }),
1115 )
1116 .unwrap();
1117 r.add_entry_with_metadata(
1119 "bad id".to_string(),
1120 3,
1121 serde_json::json!({ "checklist_id": "one" }),
1122 )
1123 .unwrap();
1124
1125 assert!(r.checklist_items().is_empty(), "none of those are items");
1126 assert!(r.open_checklist_items().is_empty());
1127 assert!(
1128 r.render_checklist().is_empty(),
1129 "and they do not render as a checklist"
1130 );
1131 }
1132
1133 #[test]
1136 fn a_checklist_caches_until_it_changes() {
1137 assert_eq!(
1138 RegionKind::Checklist.cache_hint(),
1139 crate::cache::CacheHint::UntilChanged
1140 );
1141 }
1142
1143 #[test]
1144 fn a_note_appears_in_the_render() {
1145 let mut r = checklist();
1146 let id = r.add_checklist_item("blocked".to_string(), 2).unwrap();
1147 r.note_checklist_item(id, "waiting on the manual");
1148 let rendered = r.render_checklist();
1149 assert!(
1150 rendered.contains("note: waiting on the manual"),
1151 "{rendered}"
1152 );
1153 }
1154
1155 #[test]
1158 fn an_item_over_budget_is_refused() {
1159 let mut r = Region::new("todos".to_string(), RegionKind::Checklist, 4);
1160 assert!(r.add_checklist_item("x".to_string(), 99).is_err());
1161 assert!(r.checklist_items().is_empty());
1162 }
1163
1164 #[test]
1165 fn an_added_item_starts_open_and_gets_an_id() {
1166 let mut r = checklist();
1167 let first = r
1168 .add_checklist_item("compute the fee table".to_string(), 5)
1169 .unwrap();
1170 let second = r
1171 .add_checklist_item("check the manual".to_string(), 5)
1172 .unwrap();
1173 assert_eq!((first, second), (1, 2), "ids are stable and sequential");
1174 assert_eq!(r.open_checklist_items().len(), 2);
1175 }
1176
1177 #[test]
1178 fn completing_an_item_closes_it_and_nothing_else() {
1179 let mut r = checklist();
1180 let id = r.add_checklist_item("one".to_string(), 2).unwrap();
1181 r.add_checklist_item("two".to_string(), 2).unwrap();
1182
1183 assert!(r.complete_checklist_item(id));
1184 let open = r.open_checklist_items();
1185 assert_eq!(open.len(), 1);
1186 assert_eq!(open[0].text, "two");
1187 assert_eq!(
1188 r.checklist_items().len(),
1189 2,
1190 "done items are kept, not deleted"
1191 );
1192 }
1193
1194 #[test]
1195 fn an_unknown_id_reports_failure_rather_than_ticking_something_else() {
1196 let mut r = checklist();
1199 r.add_checklist_item("one".to_string(), 2).unwrap();
1200 assert!(!r.complete_checklist_item(99));
1201 assert!(!r.note_checklist_item(99, "x"));
1202 assert_eq!(r.open_checklist_items().len(), 1);
1203 }
1204
1205 #[test]
1206 fn a_note_records_without_closing() {
1207 let mut r = checklist();
1208 let id = r
1209 .add_checklist_item("blocked thing".to_string(), 2)
1210 .unwrap();
1211 assert!(r.note_checklist_item(id, "waiting on the manual"));
1212 let item = &r.checklist_items()[0];
1213 assert!(!item.done, "a note is not a completion");
1214 assert_eq!(item.note.as_deref(), Some("waiting on the manual"));
1215 }
1216
1217 #[test]
1220 fn the_render_puts_open_items_first() {
1221 let mut r = checklist();
1222 let done = r
1223 .add_checklist_item("already finished".to_string(), 2)
1224 .unwrap();
1225 r.add_checklist_item("still to do".to_string(), 2).unwrap();
1226 r.complete_checklist_item(done);
1227
1228 let rendered = r.render_checklist();
1229 let open_at = rendered.find("still to do").expect("open item rendered");
1230 let done_at = rendered
1231 .find("already finished")
1232 .expect("done item rendered");
1233 assert!(open_at < done_at, "open before done:\n{rendered}");
1234 assert!(rendered.contains("1 open, 1 done"), "{rendered}");
1235 assert!(
1236 rendered.contains("[x]") && rendered.contains("[ ]"),
1237 "{rendered}"
1238 );
1239 }
1240
1241 #[test]
1242 fn an_empty_checklist_renders_nothing() {
1243 assert!(checklist().render_checklist().is_empty());
1245 }
1246
1247 #[test]
1250 fn ids_do_not_get_reused_after_a_drop() {
1251 let mut r = checklist();
1252 r.add_checklist_item("one".to_string(), 2).unwrap();
1253 let second = r.add_checklist_item("two".to_string(), 2).unwrap();
1254 r.content.remove(0);
1255 let third = r.add_checklist_item("three".to_string(), 2).unwrap();
1256 assert!(third > second, "a reused id would tick off the wrong item");
1257 }
1258
1259 #[test]
1260 fn test_region_creation() {
1261 let region = Region::new("test".to_string(), RegionKind::Pinned, 1000);
1262 assert_eq!(region.name, "test");
1263 assert_eq!(region.max_tokens, 1000);
1264 assert_eq!(region.current_tokens, 0);
1265 }
1266
1267 #[test]
1268 fn test_sliding_window_config() {
1269 let kind = RegionKind::SlidingWindow {
1270 max_items: 10,
1271 eviction_strategy: EvictionStrategy::PerItem,
1272 };
1273 let region = Region::new("history".to_string(), kind.clone(), 5000);
1274 assert_eq!(region.kind, kind);
1275 }
1276
1277 #[test]
1278 fn test_region_kind_equality() {
1279 assert_eq!(RegionKind::Clearable, RegionKind::Clearable);
1280 assert_eq!(
1281 RegionKind::Compacting {
1282 threshold_tokens: 500
1283 },
1284 RegionKind::Compacting {
1285 threshold_tokens: 500
1286 }
1287 );
1288 assert_eq!(
1289 RegionKind::CompactHistory {
1290 source_region: "conv".to_string()
1291 },
1292 RegionKind::CompactHistory {
1293 source_region: "conv".to_string()
1294 }
1295 );
1296 assert_ne!(RegionKind::Pinned, RegionKind::Temporary);
1297 }
1298
1299 #[test]
1300 fn custom_kind_equality_compares_script_and_persistent() {
1301 let a = RegionKind::Custom {
1302 script: "conv.rhai".to_string(),
1303 persistent: false,
1304 };
1305 assert_eq!(a, a.clone());
1306 assert_ne!(
1307 a,
1308 RegionKind::Custom {
1309 script: "other.rhai".to_string(),
1310 persistent: false,
1311 }
1312 );
1313 assert_ne!(
1314 a,
1315 RegionKind::Custom {
1316 script: "conv.rhai".to_string(),
1317 persistent: true,
1318 }
1319 );
1320 assert_ne!(a, RegionKind::Temporary);
1321 }
1322
1323 #[test]
1324 fn custom_kind_serde_round_trips() {
1325 let kind = RegionKind::Custom {
1326 script: "hooks/conv.rhai".to_string(),
1327 persistent: true,
1328 };
1329 let json = serde_json::to_string(&kind).unwrap();
1330 let back: RegionKind = serde_json::from_str(&json).unwrap();
1331 assert_eq!(kind, back);
1332 let old: RegionKind = serde_json::from_str("\"Pinned\"").unwrap();
1334 assert_eq!(old, RegionKind::Pinned);
1335 }
1336
1337 #[test]
1338 fn custom_kind_cache_hint_follows_persistent() {
1339 assert_eq!(
1340 RegionKind::Custom {
1341 script: "s.rhai".to_string(),
1342 persistent: true,
1343 }
1344 .cache_hint(),
1345 crate::cache::CacheHint::Always
1346 );
1347 assert_eq!(
1348 RegionKind::Custom {
1349 script: "s.rhai".to_string(),
1350 persistent: false,
1351 }
1352 .cache_hint(),
1353 crate::cache::CacheHint::UntilChanged
1354 );
1355 }
1356
1357 #[test]
1358 fn carry_entry_preserves_kind_metadata_key_and_timestamp() {
1359 let mut source = Region::new("conversation".to_string(), RegionKind::Temporary, 10_000);
1360 source
1361 .add_typed_entry(
1362 "result body".to_string(),
1363 10,
1364 EntryKind::ToolResult {
1365 tool_call_id: "call_1".to_string(),
1366 tool_name: "read_file".to_string(),
1367 is_error: false,
1368 },
1369 )
1370 .unwrap();
1371 let mut entry = source.content[0].clone();
1372 entry.metadata = Some(serde_json::json!({"origin": "test"}));
1373 entry.key = Some("k".to_string());
1374 let stamped = entry.timestamp;
1375
1376 let mut dest = Region::new("conversation".to_string(), RegionKind::Temporary, 10_000);
1377 dest.carry_entry(entry).unwrap();
1378
1379 let carried = &dest.content[0];
1380 assert!(matches!(
1381 &carried.kind,
1382 EntryKind::ToolResult { tool_call_id, .. } if tool_call_id == "call_1"
1383 ));
1384 assert_eq!(
1385 carried.metadata,
1386 Some(serde_json::json!({"origin": "test"}))
1387 );
1388 assert_eq!(carried.key.as_deref(), Some("k"));
1389 assert_eq!(carried.timestamp, stamped);
1390 assert_eq!(dest.current_tokens, 10);
1391 }
1392
1393 #[test]
1394 fn carry_entry_rejects_over_budget() {
1395 let mut dest = Region::new("small".to_string(), RegionKind::Temporary, 5);
1396 let mut source = Region::new("src".to_string(), RegionKind::Temporary, 100);
1397 source.add_entry("filler".to_string(), 10).unwrap();
1398 let err = dest.carry_entry(source.content[0].clone()).unwrap_err();
1399 assert_eq!(err.to_string(), "Content exceeds token budget: 10 > 5");
1400 assert!(dest.content.is_empty());
1401 assert_eq!(dest.current_tokens, 0);
1402 }
1403
1404 #[test]
1405 fn carry_entry_enforces_sliding_window_max_items() {
1406 let mut source = Region::new("src".to_string(), RegionKind::Temporary, 10_000);
1407 for i in 0..4 {
1408 source.add_entry(format!("msg{i}"), 10).unwrap();
1409 }
1410 let mut dest = Region::new(
1411 "conv".to_string(),
1412 RegionKind::SlidingWindow {
1413 max_items: 3,
1414 eviction_strategy: EvictionStrategy::PerItem,
1415 },
1416 10_000,
1417 );
1418 for entry in &source.content {
1419 dest.carry_entry(entry.clone()).unwrap();
1420 }
1421 assert_eq!(dest.content.len(), 3);
1422 assert_eq!(dest.content[0].content, "msg1");
1423 }
1424
1425 #[test]
1426 fn test_sliding_window_enforces_max_items() {
1427 let mut region = Region::new(
1428 "conv".to_string(),
1429 RegionKind::SlidingWindow {
1430 max_items: 3,
1431 eviction_strategy: EvictionStrategy::PerItem,
1432 },
1433 50000,
1434 );
1435
1436 region.add_entry("msg1".to_string(), 10).unwrap();
1437 region.add_entry("msg2".to_string(), 20).unwrap();
1438 region.add_entry("msg3".to_string(), 30).unwrap();
1439 assert_eq!(region.entry_count(), 3);
1440 assert_eq!(region.current_tokens, 60);
1441
1442 region.add_entry("msg4".to_string(), 40).unwrap();
1444 assert_eq!(region.entry_count(), 3);
1445 assert_eq!(region.content[0].content, "msg2");
1446 assert_eq!(region.content[2].content, "msg4");
1447 assert_eq!(region.current_tokens, 90); region.add_entry("msg5".to_string(), 50).unwrap();
1451 assert_eq!(region.entry_count(), 3);
1452 assert_eq!(region.content[0].content, "msg3");
1453 assert_eq!(region.current_tokens, 120); }
1455
1456 #[test]
1457 fn test_sliding_window_enforces_max_items_with_metadata() {
1458 let mut region = Region::new(
1459 "conv".to_string(),
1460 RegionKind::SlidingWindow {
1461 max_items: 2,
1462 eviction_strategy: EvictionStrategy::PerItem,
1463 },
1464 50000,
1465 );
1466
1467 region
1468 .add_entry_with_metadata("a".to_string(), 10, serde_json::json!({"idx": 1}))
1469 .unwrap();
1470 region
1471 .add_entry_with_metadata("b".to_string(), 20, serde_json::json!({"idx": 2}))
1472 .unwrap();
1473 region
1474 .add_entry_with_metadata("c".to_string(), 30, serde_json::json!({"idx": 3}))
1475 .unwrap();
1476
1477 assert_eq!(region.entry_count(), 2);
1478 assert_eq!(region.content[0].content, "b");
1479 assert_eq!(region.content[1].content, "c");
1480 assert_eq!(region.current_tokens, 50);
1481 }
1482
1483 #[test]
1484 fn test_cache_hint_pinned() {
1485 let kind = RegionKind::Pinned;
1486 assert_eq!(kind.cache_hint(), crate::cache::CacheHint::Always);
1487 }
1488
1489 #[test]
1490 fn test_cache_hint_compact_history() {
1491 let kind = RegionKind::CompactHistory {
1492 source_region: "conv".to_string(),
1493 };
1494 assert_eq!(kind.cache_hint(), crate::cache::CacheHint::Always);
1495 }
1496
1497 #[test]
1498 fn test_cache_hint_compacting() {
1499 let kind = RegionKind::Compacting {
1500 threshold_tokens: 1000,
1501 };
1502 assert_eq!(kind.cache_hint(), crate::cache::CacheHint::UntilChanged);
1503 }
1504
1505 #[test]
1506 fn test_cache_hint_sliding_window() {
1507 let kind = RegionKind::SlidingWindow {
1508 max_items: 10,
1509 eviction_strategy: EvictionStrategy::PerItem,
1510 };
1511 assert_eq!(
1512 kind.cache_hint(),
1513 crate::cache::CacheHint::SlidingPrefix {
1514 stable_fraction: 0.75
1515 }
1516 );
1517 }
1518
1519 #[test]
1520 fn test_cache_hint_temporary() {
1521 assert_eq!(
1522 RegionKind::Temporary.cache_hint(),
1523 crate::cache::CacheHint::Never
1524 );
1525 }
1526
1527 #[test]
1528 fn test_cache_hint_clearable() {
1529 assert_eq!(
1530 RegionKind::Clearable.cache_hint(),
1531 crate::cache::CacheHint::Never
1532 );
1533 }
1534
1535 #[test]
1538 fn test_with_schema_attaches_schema() {
1539 let schema = RegionSchema::new(ContentFormat::Json);
1540 let region =
1541 Region::new("data".to_string(), RegionKind::Temporary, 1000).with_schema(schema);
1542 assert!(region.schema.is_some());
1543 }
1544
1545 #[test]
1546 fn test_add_entry_rejects_content_failing_schema() {
1547 let schema = RegionSchema::new(ContentFormat::Json);
1548 let mut region =
1549 Region::new("data".to_string(), RegionKind::Temporary, 1000).with_schema(schema);
1550 let result = region.add_entry("not json".to_string(), 10);
1551 assert!(result.is_err());
1552 assert_eq!(region.entry_count(), 0);
1553 }
1554
1555 #[test]
1556 fn test_add_entry_accepts_content_passing_schema() {
1557 let schema = RegionSchema::new(ContentFormat::Json);
1558 let mut region =
1559 Region::new("data".to_string(), RegionKind::Temporary, 1000).with_schema(schema);
1560 let result = region.add_entry("{\"a\":1}".to_string(), 10);
1561 assert!(result.is_ok());
1562 assert_eq!(region.entry_count(), 1);
1563 }
1564
1565 #[test]
1566 fn test_add_entry_rejects_over_budget() {
1567 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 10);
1568 let result = region.add_entry("too much".to_string(), 20);
1569 assert_eq!(
1570 result.unwrap_err().to_string(),
1571 "Content exceeds token budget: 20 > 10"
1572 );
1573 assert_eq!(region.entry_count(), 0);
1574 }
1575
1576 #[test]
1577 fn test_add_entry_with_metadata_rejects_content_failing_schema() {
1578 let schema = RegionSchema::new(ContentFormat::Json);
1579 let mut region =
1580 Region::new("data".to_string(), RegionKind::Temporary, 1000).with_schema(schema);
1581 let result =
1582 region.add_entry_with_metadata("not json".to_string(), 10, serde_json::json!({}));
1583 assert!(result.is_err());
1584 }
1585
1586 #[test]
1587 fn test_add_entry_with_metadata_rejects_over_budget() {
1588 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 10);
1589 let result =
1590 region.add_entry_with_metadata("too much".to_string(), 20, serde_json::json!({}));
1591 assert_eq!(
1592 result.unwrap_err().to_string(),
1593 "Content exceeds token budget: 20 > 10"
1594 );
1595 }
1596
1597 #[test]
1598 fn test_add_entry_with_metadata_stores_metadata() {
1599 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
1600 region
1601 .add_entry_with_metadata("hello".to_string(), 5, serde_json::json!({"k": "v"}))
1602 .unwrap();
1603 assert_eq!(
1604 region.content[0].metadata,
1605 Some(serde_json::json!({"k": "v"}))
1606 );
1607 }
1608
1609 #[test]
1612 fn test_clear_removes_all_content_and_resets_tokens() {
1613 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
1614 region.add_entry("a".to_string(), 10).unwrap();
1615 region.add_entry("b".to_string(), 20).unwrap();
1616 assert_eq!(region.entry_count(), 2);
1617
1618 region.clear();
1619 assert_eq!(region.entry_count(), 0);
1620 assert_eq!(region.current_tokens, 0);
1621 }
1622
1623 #[test]
1624 fn test_remove_oldest_returns_and_removes_first_entry() {
1625 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
1626 region.add_entry("first".to_string(), 10).unwrap();
1627 region.add_entry("second".to_string(), 20).unwrap();
1628
1629 let removed = region.remove_oldest().unwrap();
1630 assert_eq!(removed.content, "first");
1631 assert_eq!(region.entry_count(), 1);
1632 assert_eq!(region.current_tokens, 20);
1633 }
1634
1635 #[test]
1636 fn test_remove_oldest_returns_none_when_empty() {
1637 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
1638 assert!(region.remove_oldest().is_none());
1639 }
1640
1641 #[test]
1642 fn test_needs_compaction_true_when_over_threshold() {
1643 let mut region = Region::new(
1644 "impl".to_string(),
1645 RegionKind::Compacting {
1646 threshold_tokens: 10,
1647 },
1648 1000,
1649 );
1650 region.add_entry("x".to_string(), 20).unwrap();
1651 assert!(region.needs_compaction());
1652 }
1653
1654 #[test]
1655 fn test_needs_compaction_false_when_under_threshold() {
1656 let mut region = Region::new(
1657 "impl".to_string(),
1658 RegionKind::Compacting {
1659 threshold_tokens: 100,
1660 },
1661 1000,
1662 );
1663 region.add_entry("x".to_string(), 20).unwrap();
1664 assert!(!region.needs_compaction());
1665 }
1666
1667 #[test]
1668 fn test_needs_compaction_false_for_non_compacting_kind() {
1669 let region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
1670 assert!(!region.needs_compaction());
1671 }
1672
1673 #[test]
1676 fn test_region_schema_with_custom_script() {
1677 let schema = RegionSchema::new(ContentFormat::Custom {
1678 format_name: "special".to_string(),
1679 })
1680 .with_custom_script("validate_special()".to_string());
1681 assert_eq!(schema.custom_script.as_deref(), Some("validate_special()"));
1682 }
1683
1684 #[test]
1687 fn test_validate_json_valid() {
1688 let schema = RegionSchema::new(ContentFormat::Json);
1689 assert!(schema.validate("{\"a\": 1}").is_ok());
1690 }
1691
1692 #[test]
1693 fn test_validate_json_invalid() {
1694 let schema = RegionSchema::new(ContentFormat::Json);
1695 let err = schema.validate("not json").unwrap_err();
1696 assert!(err.to_string().starts_with("Region validation failed:"));
1697 }
1698
1699 #[test]
1700 fn test_validate_mermaid_valid() {
1701 let schema = RegionSchema::new(ContentFormat::Mermaid);
1702 assert!(schema.validate("graph TD\nA-->B").is_ok());
1703 }
1704
1705 #[test]
1706 fn test_validate_mermaid_all_recognized_diagram_types() {
1707 let schema = RegionSchema::new(ContentFormat::Mermaid);
1708 for kind in [
1709 "graph",
1710 "sequenceDiagram",
1711 "classDiagram",
1712 "stateDiagram",
1713 "erDiagram",
1714 "journey",
1715 "gantt",
1716 "pie",
1717 "flowchart",
1718 ] {
1719 assert!(schema.validate(&format!("{} content", kind)).is_ok());
1720 }
1721 }
1722
1723 #[test]
1724 fn test_validate_mermaid_invalid() {
1725 let schema = RegionSchema::new(ContentFormat::Mermaid);
1726 let err = schema.validate("just some text").unwrap_err();
1727 assert!(err.to_string().starts_with("Region validation failed:"));
1728 }
1729
1730 #[test]
1731 fn test_validate_code_non_empty_is_ok() {
1732 let schema = RegionSchema::new(ContentFormat::Code {
1733 language: "rust".to_string(),
1734 });
1735 assert!(schema.validate("fn main() {}").is_ok());
1736 }
1737
1738 #[test]
1739 fn test_validate_code_empty_is_error() {
1740 let schema = RegionSchema::new(ContentFormat::Code {
1741 language: "rust".to_string(),
1742 });
1743 let err = schema.validate(" ").unwrap_err();
1744 assert!(err.to_string().starts_with("Region validation failed:"));
1745 }
1746
1747 #[test]
1748 fn test_validate_markdown_non_empty_is_ok() {
1749 let schema = RegionSchema::new(ContentFormat::Markdown);
1750 assert!(schema.validate("# Heading").is_ok());
1751 }
1752
1753 #[test]
1754 fn test_validate_markdown_empty_is_error() {
1755 let schema = RegionSchema::new(ContentFormat::Markdown);
1756 let err = schema.validate("").unwrap_err();
1757 assert!(err.to_string().starts_with("Region validation failed:"));
1758 }
1759
1760 #[test]
1761 fn test_validate_text_has_no_restrictions() {
1762 let schema = RegionSchema::new(ContentFormat::Text);
1763 assert!(schema.validate("").is_ok());
1764 assert!(schema.validate("anything at all").is_ok());
1765 }
1766
1767 #[test]
1768 fn test_validate_custom_has_no_restrictions_here() {
1769 let schema = RegionSchema::new(ContentFormat::Custom {
1770 format_name: "special".to_string(),
1771 });
1772 assert!(schema.validate("").is_ok());
1775 assert!(schema.validate("whatever").is_ok());
1776 }
1777
1778 #[test]
1781 fn test_region_schema_clone_preserves_fields() {
1782 let schema = RegionSchema::new(ContentFormat::Text).with_custom_script("s".to_string());
1783 let cloned = schema.clone();
1784 assert_eq!(cloned.custom_script.as_deref(), Some("s"));
1785 assert_eq!(cloned.format, ContentFormat::Text);
1786 }
1787
1788 #[test]
1791 fn test_region_with_taint_tracking() {
1792 let region =
1793 Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
1794 assert!(region.taint.is_some());
1795 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
1796 }
1797
1798 #[test]
1799 fn test_region_without_taint_tracking() {
1800 let region = Region::new("test".to_string(), RegionKind::Temporary, 1000);
1801 assert!(region.taint.is_none());
1802 assert_eq!(region.taint_level(), None);
1803 }
1804
1805 #[test]
1806 fn test_enable_taint_tracking() {
1807 let mut region = Region::new("test".to_string(), RegionKind::Temporary, 1000);
1808 assert!(region.taint.is_none());
1809 region.enable_taint_tracking();
1810 assert!(region.taint.is_some());
1811 region.enable_taint_tracking();
1813 assert!(region.taint.is_some());
1814 }
1815
1816 #[test]
1817 fn test_add_tainted_entry() {
1818 let mut region =
1819 Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
1820 region
1821 .add_tainted_entry(
1822 "secret data".to_string(),
1823 10,
1824 crate::taint::TaintLevel::Private,
1825 )
1826 .unwrap();
1827 assert_eq!(
1828 region.taint_level(),
1829 Some(crate::taint::TaintLevel::Private)
1830 );
1831 assert_eq!(region.entry_count(), 1);
1832 }
1833
1834 #[test]
1835 fn test_add_tainted_entry_validates_schema() {
1836 let mut region = Region::new("test".to_string(), RegionKind::Temporary, 1000)
1837 .with_taint_tracking()
1838 .with_schema(RegionSchema::new(ContentFormat::Json));
1839 let result = region.add_tainted_entry(
1840 "not json".to_string(),
1841 10,
1842 crate::taint::TaintLevel::Internal,
1843 );
1844 assert!(result.is_err());
1845 assert_eq!(region.entry_count(), 0);
1846 }
1847
1848 #[test]
1849 fn test_add_tainted_entry_checks_budget() {
1850 let mut region =
1851 Region::new("test".to_string(), RegionKind::Temporary, 10).with_taint_tracking();
1852 let result = region.add_tainted_entry(
1853 "too much".to_string(),
1854 20,
1855 crate::taint::TaintLevel::Internal,
1856 );
1857 assert!(result.is_err());
1858 }
1859
1860 #[test]
1861 fn test_add_entry_tracks_taint_as_public() {
1862 let mut region =
1863 Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
1864 region.add_entry("public data".to_string(), 10).unwrap();
1865 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
1866 }
1867
1868 #[test]
1869 fn test_taint_recovery_on_remove_oldest() {
1870 let mut region =
1871 Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
1872 region
1873 .add_tainted_entry("private".to_string(), 10, crate::taint::TaintLevel::Private)
1874 .unwrap();
1875 region
1876 .add_tainted_entry("public".to_string(), 10, crate::taint::TaintLevel::Public)
1877 .unwrap();
1878 assert_eq!(
1879 region.taint_level(),
1880 Some(crate::taint::TaintLevel::Private)
1881 );
1882
1883 region.remove_oldest(); assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
1885 }
1886
1887 #[test]
1888 fn test_taint_recovery_on_clear() {
1889 let mut region =
1890 Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
1891 region
1892 .add_tainted_entry("private".to_string(), 10, crate::taint::TaintLevel::Private)
1893 .unwrap();
1894 region.clear();
1895 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
1896 }
1897
1898 #[test]
1899 fn test_taint_recovery_on_sliding_window_eviction() {
1900 let mut region = Region::new(
1901 "conv".to_string(),
1902 RegionKind::SlidingWindow {
1903 max_items: 2,
1904 eviction_strategy: EvictionStrategy::PerItem,
1905 },
1906 50000,
1907 )
1908 .with_taint_tracking();
1909
1910 region
1911 .add_tainted_entry("private".to_string(), 10, crate::taint::TaintLevel::Private)
1912 .unwrap();
1913 region
1914 .add_tainted_entry("public1".to_string(), 10, crate::taint::TaintLevel::Public)
1915 .unwrap();
1916 assert_eq!(
1917 region.taint_level(),
1918 Some(crate::taint::TaintLevel::Private)
1919 );
1920
1921 region
1923 .add_tainted_entry("public2".to_string(), 10, crate::taint::TaintLevel::Public)
1924 .unwrap();
1925 assert_eq!(region.entry_count(), 2);
1926 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
1927 }
1928
1929 #[test]
1930 fn test_taint_field_not_serialized_when_none() {
1931 let region = Region::new("test".to_string(), RegionKind::Temporary, 1000);
1932 let json = serde_json::to_string(®ion).unwrap();
1933 assert!(!json.contains("taint"));
1934 }
1935
1936 #[test]
1937 fn test_taint_field_deserialized_as_none_when_missing() {
1938 let json = r#"{"name":"test","kind":"Temporary","content":[],"max_tokens":1000,"current_tokens":0,"schema":null}"#;
1939 let region: Region = serde_json::from_str(json).unwrap();
1940 assert!(region.taint.is_none());
1941 }
1942
1943 #[test]
1944 fn test_add_typed_tainted_entry() {
1945 let mut region = Region::new(
1946 "conversation".to_string(),
1947 RegionKind::SlidingWindow {
1948 max_items: 100,
1949 eviction_strategy: EvictionStrategy::PerItem,
1950 },
1951 1000,
1952 )
1953 .with_taint_tracking();
1954
1955 region
1956 .add_typed_tainted_entry(
1957 "secret data".to_string(),
1958 10,
1959 EntryKind::ToolResult {
1960 tool_call_id: "tc_1".to_string(),
1961 tool_name: "calendar".to_string(),
1962 is_error: false,
1963 },
1964 crate::taint::TaintLevel::Private,
1965 )
1966 .unwrap();
1967
1968 assert_eq!(region.content.len(), 1);
1969 assert_eq!(
1970 region.content[0].kind,
1971 EntryKind::ToolResult {
1972 tool_call_id: "tc_1".to_string(),
1973 tool_name: "calendar".to_string(),
1974 is_error: false,
1975 }
1976 );
1977 assert_eq!(
1978 region.taint_level(),
1979 Some(crate::taint::TaintLevel::Private)
1980 );
1981 }
1982
1983 #[test]
1987 fn serialized_tool_call_round_trips_thought_signature_and_reads_old_json() {
1988 let with = SerializedToolCall {
1989 id: "c1".into(),
1990 name: "shell".into(),
1991 arguments: serde_json::json!({"command": "ls"}),
1992 thought_signature: Some("sig".into()),
1993 };
1994 let json = serde_json::to_string(&with).unwrap();
1995 let back: SerializedToolCall = serde_json::from_str(&json).unwrap();
1996 assert_eq!(back.thought_signature.as_deref(), Some("sig"));
1997
1998 let old = r#"{"id":"c2","name":"shell","arguments":{}}"#;
2000 let back: SerializedToolCall = serde_json::from_str(old).unwrap();
2001 assert_eq!(back.thought_signature, None);
2002
2003 let without = SerializedToolCall {
2006 id: "c3".into(),
2007 name: "shell".into(),
2008 arguments: serde_json::json!({}),
2009 thought_signature: None,
2010 };
2011 assert!(
2012 !serde_json::to_string(&without)
2013 .unwrap()
2014 .contains("thought_signature")
2015 );
2016 }
2017
2018 #[test]
2019 fn test_add_typed_tainted_entry_checks_budget() {
2020 let mut region = Region::new(
2021 "conversation".to_string(),
2022 RegionKind::SlidingWindow {
2023 max_items: 100,
2024 eviction_strategy: EvictionStrategy::PerItem,
2025 },
2026 5,
2027 )
2028 .with_taint_tracking();
2029
2030 let result = region.add_typed_tainted_entry(
2031 "too large".to_string(),
2032 100,
2033 EntryKind::ToolResult {
2034 tool_call_id: "tc_1".to_string(),
2035 tool_name: "tool".to_string(),
2036 is_error: false,
2037 },
2038 crate::taint::TaintLevel::Internal,
2039 );
2040 assert!(result.is_err());
2041 }
2042
2043 #[test]
2044 fn test_add_typed_tainted_entry_validates_schema() {
2045 let mut region = Region::new("test".to_string(), RegionKind::Pinned, 1000)
2046 .with_taint_tracking()
2047 .with_schema(RegionSchema::new(ContentFormat::Json));
2048
2049 let result = region.add_typed_tainted_entry(
2051 "not json".to_string(),
2052 5,
2053 EntryKind::Text,
2054 crate::taint::TaintLevel::Public,
2055 );
2056 assert!(result.is_err());
2057 }
2058
2059 #[test]
2060 fn test_add_typed_tainted_entry_without_taint_tracking() {
2061 let mut region = Region::new(
2064 "conversation".to_string(),
2065 RegionKind::SlidingWindow {
2066 max_items: 100,
2067 eviction_strategy: EvictionStrategy::PerItem,
2068 },
2069 1000,
2070 );
2071 region
2074 .add_typed_tainted_entry(
2075 "data".to_string(),
2076 10,
2077 EntryKind::Text,
2078 crate::taint::TaintLevel::Private,
2079 )
2080 .unwrap();
2081
2082 assert_eq!(region.content.len(), 1);
2083 assert_eq!(region.taint_level(), None); }
2085
2086 #[test]
2089 fn test_turn_group_size_at_assistant_with_tool_results() {
2090 let mut region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
2091 region
2092 .add_typed_entry(
2093 "assistant response".to_string(),
2094 10,
2095 EntryKind::AssistantTurn {
2096 tool_calls: vec![
2097 SerializedToolCall {
2098 id: "tc_1".to_string(),
2099 name: "read_file".to_string(),
2100 arguments: serde_json::json!({}),
2101 thought_signature: None,
2102 },
2103 SerializedToolCall {
2104 id: "tc_2".to_string(),
2105 name: "write_file".to_string(),
2106 arguments: serde_json::json!({}),
2107 thought_signature: None,
2108 },
2109 ],
2110 },
2111 )
2112 .unwrap();
2113 region
2114 .add_typed_entry(
2115 "result 1".to_string(),
2116 5,
2117 EntryKind::ToolResult {
2118 tool_call_id: "tc_1".to_string(),
2119 tool_name: "read_file".to_string(),
2120 is_error: false,
2121 },
2122 )
2123 .unwrap();
2124 region
2125 .add_typed_entry(
2126 "result 2".to_string(),
2127 5,
2128 EntryKind::ToolResult {
2129 tool_call_id: "tc_2".to_string(),
2130 tool_name: "write_file".to_string(),
2131 is_error: false,
2132 },
2133 )
2134 .unwrap();
2135
2136 assert_eq!(region.turn_group_size_at(0), 3);
2137 }
2138
2139 #[test]
2140 fn test_turn_group_size_at_assistant_at_end() {
2141 let mut region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
2142 region
2143 .add_typed_entry(
2144 "assistant with no tools".to_string(),
2145 10,
2146 EntryKind::AssistantTurn { tool_calls: vec![] },
2147 )
2148 .unwrap();
2149
2150 assert_eq!(region.turn_group_size_at(0), 1);
2151 }
2152
2153 #[test]
2154 fn test_turn_group_size_at_out_of_bounds() {
2155 let region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
2156 assert_eq!(region.turn_group_size_at(0), 0);
2157 assert_eq!(region.turn_group_size_at(99), 0);
2158 }
2159
2160 #[test]
2161 fn test_turn_group_size_at_non_assistant_entries() {
2162 let mut region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
2163 region
2164 .add_typed_entry("hello".to_string(), 5, EntryKind::Text)
2165 .unwrap();
2166 region
2167 .add_typed_entry("hi".to_string(), 5, EntryKind::UserMessage)
2168 .unwrap();
2169 region
2170 .add_typed_entry(
2171 "orphan result".to_string(),
2172 5,
2173 EntryKind::ToolResult {
2174 tool_call_id: "tc_x".to_string(),
2175 tool_name: "tool".to_string(),
2176 is_error: false,
2177 },
2178 )
2179 .unwrap();
2180
2181 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); }
2185
2186 #[test]
2189 fn test_remove_oldest_evicts_entire_turn_group() {
2190 let mut region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
2191 region
2193 .add_typed_entry(
2194 "assistant".to_string(),
2195 100,
2196 EntryKind::AssistantTurn {
2197 tool_calls: vec![
2198 SerializedToolCall {
2199 id: "tc_1".to_string(),
2200 name: "read_file".to_string(),
2201 arguments: serde_json::json!({}),
2202 thought_signature: None,
2203 },
2204 SerializedToolCall {
2205 id: "tc_2".to_string(),
2206 name: "list_dir".to_string(),
2207 arguments: serde_json::json!({}),
2208 thought_signature: None,
2209 },
2210 ],
2211 },
2212 )
2213 .unwrap();
2214 region
2215 .add_typed_entry(
2216 "result 1".to_string(),
2217 30,
2218 EntryKind::ToolResult {
2219 tool_call_id: "tc_1".to_string(),
2220 tool_name: "read_file".to_string(),
2221 is_error: false,
2222 },
2223 )
2224 .unwrap();
2225 region
2226 .add_typed_entry(
2227 "result 2".to_string(),
2228 20,
2229 EntryKind::ToolResult {
2230 tool_call_id: "tc_2".to_string(),
2231 tool_name: "list_dir".to_string(),
2232 is_error: false,
2233 },
2234 )
2235 .unwrap();
2236 region
2238 .add_typed_entry("user msg".to_string(), 10, EntryKind::UserMessage)
2239 .unwrap();
2240
2241 assert_eq!(region.entry_count(), 4);
2242 assert_eq!(region.current_tokens, 160);
2243
2244 let removed = region.remove_oldest().unwrap();
2245 assert_eq!(removed.content, "assistant");
2248 assert_eq!(removed.tokens, 100 + 30 + 20); assert_eq!(region.entry_count(), 1);
2251 assert_eq!(region.content[0].content, "user msg");
2252 assert_eq!(region.current_tokens, 10);
2253 }
2254
2255 #[test]
2258 fn test_remove_oldest_turn_group_calls_taint_remove_for_each_entry() {
2259 let mut region =
2260 Region::new("conv".to_string(), RegionKind::Temporary, 50000).with_taint_tracking();
2261
2262 region
2264 .add_typed_tainted_entry(
2265 "assistant".to_string(),
2266 10,
2267 EntryKind::AssistantTurn {
2268 tool_calls: vec![SerializedToolCall {
2269 id: "tc_1".to_string(),
2270 name: "tool".to_string(),
2271 arguments: serde_json::json!({}),
2272 thought_signature: None,
2273 }],
2274 },
2275 crate::taint::TaintLevel::Private,
2276 )
2277 .unwrap();
2278 region
2279 .add_typed_tainted_entry(
2280 "result".to_string(),
2281 5,
2282 EntryKind::ToolResult {
2283 tool_call_id: "tc_1".to_string(),
2284 tool_name: "tool".to_string(),
2285 is_error: false,
2286 },
2287 crate::taint::TaintLevel::Internal,
2288 )
2289 .unwrap();
2290 region
2291 .add_tainted_entry(
2292 "public stuff".to_string(),
2293 5,
2294 crate::taint::TaintLevel::Public,
2295 )
2296 .unwrap();
2297
2298 assert_eq!(
2299 region.taint_level(),
2300 Some(crate::taint::TaintLevel::Private)
2301 );
2302 assert_eq!(region.taint.as_ref().unwrap().entry_count(), 3);
2303
2304 let removed = region.remove_oldest().unwrap();
2306 assert_eq!(removed.content, "assistant");
2307 assert_eq!(region.entry_count(), 1);
2308 assert_eq!(region.taint.as_ref().unwrap().entry_count(), 1);
2311 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
2312 }
2313
2314 #[test]
2317 fn test_sliding_window_evicts_entire_turn_group() {
2318 let mut region = Region::new(
2319 "conv".to_string(),
2320 RegionKind::SlidingWindow {
2321 max_items: 3,
2322 eviction_strategy: EvictionStrategy::PerItem,
2323 },
2324 50000,
2325 );
2326
2327 region
2329 .add_typed_entry(
2330 "assistant".to_string(),
2331 10,
2332 EntryKind::AssistantTurn {
2333 tool_calls: vec![
2334 SerializedToolCall {
2335 id: "tc_1".to_string(),
2336 name: "t1".to_string(),
2337 arguments: serde_json::json!({}),
2338 thought_signature: None,
2339 },
2340 SerializedToolCall {
2341 id: "tc_2".to_string(),
2342 name: "t2".to_string(),
2343 arguments: serde_json::json!({}),
2344 thought_signature: None,
2345 },
2346 ],
2347 },
2348 )
2349 .unwrap();
2350 region
2351 .add_typed_entry(
2352 "r1".to_string(),
2353 5,
2354 EntryKind::ToolResult {
2355 tool_call_id: "tc_1".to_string(),
2356 tool_name: "t1".to_string(),
2357 is_error: false,
2358 },
2359 )
2360 .unwrap();
2361 region
2362 .add_typed_entry(
2363 "r2".to_string(),
2364 5,
2365 EntryKind::ToolResult {
2366 tool_call_id: "tc_2".to_string(),
2367 tool_name: "t2".to_string(),
2368 is_error: false,
2369 },
2370 )
2371 .unwrap();
2372
2373 assert_eq!(region.entry_count(), 3);
2374
2375 region
2378 .add_typed_entry("user msg".to_string(), 15, EntryKind::UserMessage)
2379 .unwrap();
2380
2381 assert_eq!(region.entry_count(), 1);
2383 assert_eq!(region.content[0].content, "user msg");
2384 assert_eq!(region.current_tokens, 15);
2385 }
2386
2387 #[test]
2390 fn test_add_entry_with_metadata_tracks_taint_as_public() {
2391 let mut region =
2392 Region::new("data".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
2393
2394 region
2395 .add_entry_with_metadata("content".to_string(), 10, serde_json::json!({"key": "val"}))
2396 .unwrap();
2397
2398 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
2399 assert_eq!(region.taint.as_ref().unwrap().entry_count(), 1);
2400 assert_eq!(
2401 region.taint.as_ref().unwrap().entry_taint(0),
2402 Some(crate::taint::TaintLevel::Public)
2403 );
2404 }
2405
2406 #[test]
2409 fn test_add_typed_entry_tracks_taint_as_public() {
2410 let mut region =
2411 Region::new("conv".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
2412
2413 region
2414 .add_typed_entry(
2415 "assistant response".to_string(),
2416 10,
2417 EntryKind::AssistantTurn { tool_calls: vec![] },
2418 )
2419 .unwrap();
2420
2421 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
2422 assert_eq!(region.taint.as_ref().unwrap().entry_count(), 1);
2423 assert_eq!(
2424 region.taint.as_ref().unwrap().entry_taint(0),
2425 Some(crate::taint::TaintLevel::Public)
2426 );
2427 }
2428
2429 #[test]
2432 fn test_per_item_strategy_evicts_one_at_a_time() {
2433 let mut region = Region::new(
2434 "conv".to_string(),
2435 RegionKind::SlidingWindow {
2436 max_items: 3,
2437 eviction_strategy: EvictionStrategy::PerItem,
2438 },
2439 50000,
2440 );
2441 for i in 0..5 {
2442 region.add_entry(format!("msg{}", i), 10).unwrap();
2443 }
2444 assert_eq!(region.entry_count(), 3);
2445 assert_eq!(region.content[0].content, "msg2");
2446 assert_eq!(region.content[1].content, "msg3");
2447 assert_eq!(region.content[2].content, "msg4");
2448 }
2449
2450 #[test]
2451 fn test_bulk_eviction_triggers_on_overflow() {
2452 let mut region = Region::new(
2453 "conv".to_string(),
2454 RegionKind::SlidingWindow {
2455 max_items: 5,
2456 eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
2457 },
2458 50000,
2459 );
2460 for i in 0..8 {
2463 region.add_entry(format!("msg{}", i), 10).unwrap();
2464 }
2465 assert_eq!(region.entry_count(), 8);
2466
2467 region.add_entry("msg8".to_string(), 10).unwrap();
2469 assert_eq!(region.entry_count(), 5);
2470 assert_eq!(region.content[0].content, "msg4");
2471 }
2472
2473 #[test]
2474 fn test_bulk_eviction_respects_turn_groups() {
2475 let mut region = Region::new(
2476 "conv".to_string(),
2477 RegionKind::SlidingWindow {
2478 max_items: 3,
2479 eviction_strategy: EvictionStrategy::Bulk { overflow: 2 },
2480 },
2481 50000,
2482 );
2483 region
2485 .add_typed_entry(
2486 "assistant".to_string(),
2487 10,
2488 EntryKind::AssistantTurn {
2489 tool_calls: vec![SerializedToolCall {
2490 id: "tc1".to_string(),
2491 name: "tool".to_string(),
2492 arguments: serde_json::json!({}),
2493 thought_signature: None,
2494 }],
2495 },
2496 )
2497 .unwrap();
2498 region
2499 .add_typed_entry(
2500 "result".to_string(),
2501 5,
2502 EntryKind::ToolResult {
2503 tool_call_id: "tc1".to_string(),
2504 tool_name: "tool".to_string(),
2505 is_error: false,
2506 },
2507 )
2508 .unwrap();
2509 region.add_entry("msg2".to_string(), 10).unwrap();
2511 region.add_entry("msg3".to_string(), 10).unwrap();
2512 region.add_entry("msg4".to_string(), 10).unwrap();
2513 assert_eq!(region.entry_count(), 5);
2515
2516 region.add_entry("msg5".to_string(), 10).unwrap();
2518 assert_eq!(region.entry_count(), 3);
2521 assert_eq!(region.content[0].content, "msg3");
2522 }
2523
2524 #[test]
2525 fn test_bulk_eviction_under_overflow_no_eviction() {
2526 let mut region = Region::new(
2527 "conv".to_string(),
2528 RegionKind::SlidingWindow {
2529 max_items: 5,
2530 eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
2531 },
2532 50000,
2533 );
2534 for i in 0..7 {
2536 region.add_entry(format!("msg{}", i), 10).unwrap();
2537 }
2538 assert_eq!(region.entry_count(), 7);
2540 }
2541
2542 #[test]
2543 fn test_compact_sets_needs_message_compaction_flag() {
2544 let mut region = Region::new(
2545 "conv".to_string(),
2546 RegionKind::SlidingWindow {
2547 max_items: 5,
2548 eviction_strategy: EvictionStrategy::Compact { compact_count: 3 },
2549 },
2550 50000,
2551 );
2552 assert!(!region.needs_message_compaction);
2553
2554 for i in 0..9 {
2556 region.add_entry(format!("msg{}", i), 10).unwrap();
2557 }
2558 assert!(region.needs_message_compaction);
2559 assert_eq!(region.entry_count(), 9);
2561 }
2562
2563 #[test]
2564 fn test_compact_fallback_to_bulk_eviction() {
2565 let mut region = Region::new(
2566 "conv".to_string(),
2567 RegionKind::SlidingWindow {
2568 max_items: 5,
2569 eviction_strategy: EvictionStrategy::Compact { compact_count: 3 },
2570 },
2571 50000,
2572 );
2573 for i in 0..12 {
2576 region.add_entry(format!("msg{}", i), 10).unwrap();
2577 }
2578 assert_eq!(region.entry_count(), 5);
2580 assert_eq!(region.content[0].content, "msg7");
2581 assert!(!region.needs_message_compaction);
2583 }
2584
2585 #[test]
2586 fn test_eviction_strategy_default_is_per_item() {
2587 assert_eq!(EvictionStrategy::default(), EvictionStrategy::PerItem);
2588 }
2589
2590 #[test]
2591 fn test_remove_entries_by_prefix() {
2592 let mut region = Region::new("system".to_string(), RegionKind::Pinned, 50000);
2593 region
2594 .add_entry("[Stage instructions: Be terse.]".to_string(), 10)
2595 .unwrap();
2596 region
2597 .add_entry("Core identity block".to_string(), 20)
2598 .unwrap();
2599 region
2600 .add_entry("[Stage instructions: Be verbose.]".to_string(), 15)
2601 .unwrap();
2602
2603 assert_eq!(region.entry_count(), 3);
2604 region.remove_entries_by_prefix("[Stage instructions:");
2605 assert_eq!(region.entry_count(), 1);
2606 assert_eq!(region.content[0].content, "Core identity block");
2607 assert_eq!(region.current_tokens, 20);
2608 }
2609
2610 #[test]
2611 fn test_remove_entries_by_prefix_with_taint_tracking() {
2612 let mut region =
2613 Region::new("system".to_string(), RegionKind::Pinned, 50000).with_taint_tracking();
2614 region
2615 .add_tainted_entry(
2616 "[Stage instructions: Be terse.]".to_string(),
2617 10,
2618 crate::taint::TaintLevel::Private,
2619 )
2620 .unwrap();
2621 region
2622 .add_tainted_entry(
2623 "Core identity block".to_string(),
2624 20,
2625 crate::taint::TaintLevel::Public,
2626 )
2627 .unwrap();
2628 region
2629 .add_tainted_entry(
2630 "[Stage instructions: Be verbose.]".to_string(),
2631 15,
2632 crate::taint::TaintLevel::Internal,
2633 )
2634 .unwrap();
2635
2636 assert_eq!(region.entry_count(), 3);
2637 assert_eq!(
2638 region.taint_level(),
2639 Some(crate::taint::TaintLevel::Private)
2640 );
2641
2642 region.remove_entries_by_prefix("[Stage instructions:");
2643 assert_eq!(region.entry_count(), 1);
2644 assert_eq!(region.content[0].content, "Core identity block");
2645 assert_eq!(region.current_tokens, 20);
2646 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
2648 assert_eq!(region.taint.as_ref().unwrap().entry_count(), 1);
2649 }
2650
2651 #[test]
2652 fn test_compact_below_threshold_no_flag() {
2653 let mut region = Region::new(
2655 "conv".to_string(),
2656 RegionKind::SlidingWindow {
2657 max_items: 5,
2658 eviction_strategy: EvictionStrategy::Compact { compact_count: 3 },
2659 },
2660 50000,
2661 );
2662 for i in 0..8 {
2663 region.add_entry(format!("msg{}", i), 10).unwrap();
2664 }
2665 assert!(!region.needs_message_compaction);
2667 assert_eq!(region.entry_count(), 8);
2668 }
2669
2670 #[test]
2671 fn test_bulk_eviction_with_taint_tracking() {
2672 let mut region = Region::new(
2673 "conv".to_string(),
2674 RegionKind::SlidingWindow {
2675 max_items: 3,
2676 eviction_strategy: EvictionStrategy::Bulk { overflow: 2 },
2677 },
2678 50000,
2679 )
2680 .with_taint_tracking();
2681
2682 region
2684 .add_tainted_entry("private".to_string(), 10, crate::taint::TaintLevel::Private)
2685 .unwrap();
2686 for i in 1..5 {
2687 region
2688 .add_tainted_entry(format!("pub{}", i), 10, crate::taint::TaintLevel::Public)
2689 .unwrap();
2690 }
2691 assert_eq!(region.entry_count(), 5);
2692
2693 region
2695 .add_tainted_entry("pub5".to_string(), 10, crate::taint::TaintLevel::Public)
2696 .unwrap();
2697 assert_eq!(region.entry_count(), 3);
2698 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
2700 }
2701
2702 #[test]
2703 fn test_eviction_strategy_serde_roundtrip() {
2704 let bulk = EvictionStrategy::Bulk { overflow: 5 };
2705 let json = serde_json::to_string(&bulk).unwrap();
2706 let parsed: EvictionStrategy = serde_json::from_str(&json).unwrap();
2707 assert_eq!(parsed, bulk);
2708
2709 let compact = EvictionStrategy::Compact { compact_count: 10 };
2710 let json = serde_json::to_string(&compact).unwrap();
2711 let parsed: EvictionStrategy = serde_json::from_str(&json).unwrap();
2712 assert_eq!(parsed, compact);
2713
2714 let per_item = EvictionStrategy::PerItem;
2715 let json = serde_json::to_string(&per_item).unwrap();
2716 let parsed: EvictionStrategy = serde_json::from_str(&json).unwrap();
2717 assert_eq!(parsed, per_item);
2718 }
2719
2720 #[test]
2721 fn test_sliding_window_kind_equality_with_eviction_strategy() {
2722 assert_eq!(
2723 RegionKind::SlidingWindow {
2724 max_items: 10,
2725 eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
2726 },
2727 RegionKind::SlidingWindow {
2728 max_items: 10,
2729 eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
2730 }
2731 );
2732 assert_ne!(
2733 RegionKind::SlidingWindow {
2734 max_items: 10,
2735 eviction_strategy: EvictionStrategy::PerItem,
2736 },
2737 RegionKind::SlidingWindow {
2738 max_items: 10,
2739 eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
2740 }
2741 );
2742 }
2743
2744 #[test]
2745 fn test_needs_message_compaction_default_false() {
2746 let region = Region::new("conv".to_string(), RegionKind::Temporary, 1000);
2747 assert!(!region.needs_message_compaction);
2748 }
2749
2750 #[test]
2753 fn test_add_typed_entry_validates_schema() {
2754 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000)
2755 .with_schema(RegionSchema::new(ContentFormat::Json));
2756 let result = region.add_typed_entry("not json".to_string(), 5, EntryKind::Text);
2757 assert!(result.is_err());
2758 assert_eq!(region.entry_count(), 0);
2759 }
2760
2761 #[test]
2762 fn test_add_typed_entry_checks_budget() {
2763 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 10);
2764 let result = region.add_typed_entry("too big".to_string(), 20, EntryKind::UserMessage);
2765 assert!(result.is_err());
2766 assert_eq!(region.entry_count(), 0);
2767 }
2768
2769 #[test]
2770 fn test_add_tainted_entry_without_taint_tracking() {
2771 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
2773 region
2774 .add_tainted_entry("data".to_string(), 10, crate::taint::TaintLevel::Private)
2775 .unwrap();
2776 assert_eq!(region.entry_count(), 1);
2777 assert_eq!(region.taint_level(), None);
2778 }
2779
2780 #[test]
2781 fn test_remove_entries_by_prefix_no_match() {
2782 let mut region = Region::new("system".to_string(), RegionKind::Pinned, 50000);
2783 region.add_entry("Keep this".to_string(), 10).unwrap();
2784 region.add_entry("And this".to_string(), 20).unwrap();
2785 region.remove_entries_by_prefix("[Stage instructions:");
2786 assert_eq!(region.entry_count(), 2);
2787 assert_eq!(region.current_tokens, 30);
2788 }
2789
2790 #[test]
2793 fn test_hashmap_region_upsert_and_get() {
2794 let mut region = Region::new(
2795 "files".to_string(),
2796 RegionKind::HashMap { max_entries: None },
2797 10000,
2798 );
2799 region
2800 .upsert_by_key("src/main.rs", "fn main() {}".to_string(), 10)
2801 .unwrap();
2802 region
2803 .upsert_by_key("src/lib.rs", "pub mod foo;".to_string(), 8)
2804 .unwrap();
2805
2806 assert_eq!(region.entry_count(), 2);
2807 assert_eq!(region.current_tokens, 18);
2808
2809 let entry = region.get_by_key("src/main.rs").unwrap();
2810 assert_eq!(entry.content, "fn main() {}");
2811 assert_eq!(entry.key.as_deref(), Some("src/main.rs"));
2812 }
2813
2814 #[test]
2815 fn test_hashmap_region_upsert_replaces_existing() {
2816 let mut region = Region::new(
2817 "files".to_string(),
2818 RegionKind::HashMap { max_entries: None },
2819 10000,
2820 );
2821 region
2822 .upsert_by_key("file.rs", "version 1".to_string(), 10)
2823 .unwrap();
2824 assert_eq!(region.current_tokens, 10);
2825
2826 region
2827 .upsert_by_key("file.rs", "version 2".to_string(), 15)
2828 .unwrap();
2829 assert_eq!(region.entry_count(), 1);
2830 assert_eq!(region.current_tokens, 15);
2831 assert_eq!(region.get_by_key("file.rs").unwrap().content, "version 2");
2832 }
2833
2834 #[test]
2835 fn test_hashmap_region_remove_by_key() {
2836 let mut region = Region::new(
2837 "files".to_string(),
2838 RegionKind::HashMap { max_entries: None },
2839 10000,
2840 );
2841 region.upsert_by_key("a.rs", "aaa".to_string(), 10).unwrap();
2842 region.upsert_by_key("b.rs", "bbb".to_string(), 20).unwrap();
2843
2844 assert!(region.remove_by_key("a.rs"));
2845 assert_eq!(region.entry_count(), 1);
2846 assert_eq!(region.current_tokens, 20);
2847 assert!(region.get_by_key("a.rs").is_none());
2848 assert!(!region.remove_by_key("nonexistent"));
2849 }
2850
2851 #[test]
2852 fn test_hashmap_region_keys() {
2853 let mut region = Region::new(
2854 "files".to_string(),
2855 RegionKind::HashMap { max_entries: None },
2856 10000,
2857 );
2858 region.upsert_by_key("x.rs", "x".to_string(), 5).unwrap();
2859 region.upsert_by_key("y.rs", "y".to_string(), 5).unwrap();
2860
2861 let keys = region.keys();
2862 assert_eq!(keys.len(), 2);
2863 assert!(keys.contains(&"x.rs"));
2864 assert!(keys.contains(&"y.rs"));
2865 }
2866
2867 #[test]
2868 fn test_hashmap_region_lru_eviction_on_max_tokens() {
2869 let mut region = Region::new(
2870 "files".to_string(),
2871 RegionKind::HashMap { max_entries: None },
2872 30, );
2874 region.upsert_by_key("a.rs", "aaa".to_string(), 10).unwrap();
2875 region.content[0].timestamp -= 100;
2877 region.upsert_by_key("b.rs", "bbb".to_string(), 10).unwrap();
2878 region.upsert_by_key("c.rs", "ccc".to_string(), 10).unwrap();
2879 assert_eq!(region.entry_count(), 3);
2880 assert_eq!(region.current_tokens, 30);
2881
2882 region.upsert_by_key("d.rs", "ddd".to_string(), 10).unwrap();
2884 assert_eq!(region.entry_count(), 3);
2885 assert!(region.get_by_key("a.rs").is_none());
2886 assert!(region.get_by_key("d.rs").is_some());
2887 }
2888
2889 #[test]
2890 fn test_hashmap_region_max_entries_eviction() {
2891 let mut region = Region::new(
2892 "files".to_string(),
2893 RegionKind::HashMap {
2894 max_entries: Some(2),
2895 },
2896 10000,
2897 );
2898 region.upsert_by_key("a.rs", "aaa".to_string(), 10).unwrap();
2899 region.content[0].timestamp -= 100; region.upsert_by_key("b.rs", "bbb".to_string(), 10).unwrap();
2901 assert_eq!(region.entry_count(), 2);
2902
2903 region.upsert_by_key("c.rs", "ccc".to_string(), 10).unwrap();
2905 assert_eq!(region.entry_count(), 2);
2906 assert!(region.get_by_key("a.rs").is_none());
2907 assert!(region.get_by_key("c.rs").is_some());
2908 }
2909
2910 #[test]
2911 fn test_hashmap_region_upsert_too_large_for_budget() {
2912 let mut region = Region::new(
2913 "files".to_string(),
2914 RegionKind::HashMap { max_entries: None },
2915 5, );
2917 let result = region.upsert_by_key("big.rs", "huge content".to_string(), 100);
2918 assert!(result.is_err());
2919 }
2920
2921 #[test]
2922 fn test_hashmap_region_kind_equality() {
2923 assert_eq!(
2924 RegionKind::HashMap {
2925 max_entries: Some(10)
2926 },
2927 RegionKind::HashMap {
2928 max_entries: Some(10)
2929 }
2930 );
2931 assert_ne!(
2932 RegionKind::HashMap {
2933 max_entries: Some(10)
2934 },
2935 RegionKind::HashMap {
2936 max_entries: Some(20)
2937 }
2938 );
2939 assert_ne!(
2940 RegionKind::HashMap { max_entries: None },
2941 RegionKind::Pinned
2942 );
2943 }
2944
2945 #[test]
2946 fn test_hashmap_cache_hint() {
2947 let kind = RegionKind::HashMap { max_entries: None };
2948 assert_eq!(kind.cache_hint(), crate::cache::CacheHint::UntilChanged);
2949 }
2950
2951 #[test]
2952 fn test_region_entry_key_default_none() {
2953 let mut region = Region::new("test".to_string(), RegionKind::Temporary, 1000);
2954 region.add_entry("content".to_string(), 10).unwrap();
2955 assert!(region.content[0].key.is_none());
2956 }
2957
2958 #[test]
2959 fn test_region_entry_key_serde_skip_when_none() {
2960 let entry = RegionEntry {
2961 content: "test".to_string(),
2962 tokens: 5,
2963 timestamp: 0,
2964 metadata: None,
2965 kind: EntryKind::default(),
2966 key: None,
2967 };
2968 let json = serde_json::to_string(&entry).unwrap();
2969 assert!(!json.contains("key"));
2970 }
2971
2972 #[test]
2973 fn test_region_entry_key_serde_roundtrip() {
2974 let entry = RegionEntry {
2975 content: "test".to_string(),
2976 tokens: 5,
2977 timestamp: 0,
2978 metadata: None,
2979 kind: EntryKind::default(),
2980 key: Some("mykey".to_string()),
2981 };
2982 let json = serde_json::to_string(&entry).unwrap();
2983 assert!(json.contains("mykey"));
2984 let back: RegionEntry = serde_json::from_str(&json).unwrap();
2985 assert_eq!(back.key.as_deref(), Some("mykey"));
2986 }
2987
2988 #[test]
2991 fn test_hashmap_region_creation_and_basic_properties() {
2992 let region = Region::new(
2993 "lookup".to_string(),
2994 RegionKind::HashMap {
2995 max_entries: Some(5),
2996 },
2997 2000,
2998 );
2999 assert_eq!(region.name, "lookup");
3000 assert_eq!(
3001 region.kind,
3002 RegionKind::HashMap {
3003 max_entries: Some(5)
3004 }
3005 );
3006 assert_eq!(region.max_tokens, 2000);
3007 assert_eq!(region.current_tokens, 0);
3008 assert_eq!(region.entry_count(), 0);
3009 assert!(region.content.is_empty());
3010 }
3011
3012 #[test]
3013 fn test_hashmap_upsert_insert_new_entry() {
3014 let mut region = Region::new(
3015 "store".to_string(),
3016 RegionKind::HashMap {
3017 max_entries: Some(5),
3018 },
3019 5000,
3020 );
3021 region
3022 .upsert_by_key("config.toml", "[package]\nname = \"foo\"".to_string(), 12)
3023 .unwrap();
3024
3025 assert_eq!(region.entry_count(), 1);
3026 assert_eq!(region.current_tokens, 12);
3027
3028 let entry = region.get_by_key("config.toml").unwrap();
3029 assert_eq!(entry.content, "[package]\nname = \"foo\"");
3030 assert_eq!(entry.tokens, 12);
3031 assert_eq!(entry.key.as_deref(), Some("config.toml"));
3032 }
3033
3034 #[test]
3035 fn test_hashmap_upsert_update_existing_entry() {
3036 let mut region = Region::new(
3037 "store".to_string(),
3038 RegionKind::HashMap { max_entries: None },
3039 5000,
3040 );
3041 region
3042 .upsert_by_key("readme.md", "# Old".to_string(), 20)
3043 .unwrap();
3044 assert_eq!(region.current_tokens, 20);
3045
3046 region
3047 .upsert_by_key("readme.md", "# New and improved".to_string(), 35)
3048 .unwrap();
3049 assert_eq!(region.entry_count(), 1);
3050 assert_eq!(region.current_tokens, 35);
3051
3052 let entry = region.get_by_key("readme.md").unwrap();
3053 assert_eq!(entry.content, "# New and improved");
3054 assert_eq!(entry.tokens, 35);
3055 }
3056
3057 #[test]
3058 fn test_hashmap_upsert_lru_eviction_on_max_tokens() {
3059 let mut region = Region::new(
3060 "files".to_string(),
3061 RegionKind::HashMap { max_entries: None },
3062 100, );
3064
3065 region
3067 .upsert_by_key("first.rs", "first content".to_string(), 40)
3068 .unwrap();
3069 region.content[0].timestamp -= 200; region
3072 .upsert_by_key("second.rs", "second content".to_string(), 40)
3073 .unwrap();
3074 region.content[1].timestamp -= 100; region
3077 .upsert_by_key("third.rs", "third content".to_string(), 20)
3078 .unwrap();
3079 region
3083 .upsert_by_key("fourth.rs", "fourth content".to_string(), 30)
3084 .unwrap();
3085
3086 assert!(region.get_by_key("first.rs").is_none());
3088 assert!(region.get_by_key("fourth.rs").is_some());
3089 assert!(region.current_tokens <= 100);
3091 }
3092
3093 #[test]
3094 fn test_hashmap_upsert_max_entries_enforcement() {
3095 let mut region = Region::new(
3096 "cache".to_string(),
3097 RegionKind::HashMap {
3098 max_entries: Some(2),
3099 },
3100 50000,
3101 );
3102
3103 region
3104 .upsert_by_key("alpha", "aaa".to_string(), 10)
3105 .unwrap();
3106 region.content[0].timestamp -= 200; region.upsert_by_key("beta", "bbb".to_string(), 10).unwrap();
3109 region.content[1].timestamp -= 100;
3110
3111 region
3112 .upsert_by_key("gamma", "ccc".to_string(), 10)
3113 .unwrap();
3114
3115 assert_eq!(region.entry_count(), 2);
3117 assert!(region.get_by_key("alpha").is_none());
3118 assert!(region.get_by_key("beta").is_some());
3119 assert!(region.get_by_key("gamma").is_some());
3120 }
3121
3122 #[test]
3123 fn test_hashmap_get_by_key_found_and_not_found() {
3124 let mut region = Region::new(
3125 "data".to_string(),
3126 RegionKind::HashMap { max_entries: None },
3127 5000,
3128 );
3129 region
3130 .upsert_by_key("exists", "hello".to_string(), 5)
3131 .unwrap();
3132
3133 let found = region.get_by_key("exists");
3135 assert!(found.is_some());
3136 assert_eq!(found.unwrap().content, "hello");
3137
3138 let missing = region.get_by_key("does_not_exist");
3140 assert!(missing.is_none());
3141 }
3142
3143 #[test]
3144 fn test_hashmap_remove_by_key_exists() {
3145 let mut region = Region::new(
3146 "data".to_string(),
3147 RegionKind::HashMap { max_entries: None },
3148 5000,
3149 );
3150 region
3151 .upsert_by_key("target", "remove me".to_string(), 25)
3152 .unwrap();
3153 assert_eq!(region.current_tokens, 25);
3154
3155 let removed = region.remove_by_key("target");
3156 assert!(removed);
3157 assert_eq!(region.entry_count(), 0);
3158 assert_eq!(region.current_tokens, 0);
3159 assert!(region.get_by_key("target").is_none());
3160 }
3161
3162 #[test]
3163 fn test_hashmap_remove_by_key_does_not_exist() {
3164 let mut region = Region::new(
3165 "data".to_string(),
3166 RegionKind::HashMap { max_entries: None },
3167 5000,
3168 );
3169 let removed = region.remove_by_key("ghost");
3170 assert!(!removed);
3171 }
3172
3173 #[test]
3174 fn test_hashmap_keys_empty_populated_after_removal() {
3175 let mut region = Region::new(
3176 "data".to_string(),
3177 RegionKind::HashMap { max_entries: None },
3178 5000,
3179 );
3180
3181 assert!(region.keys().is_empty());
3183
3184 region.upsert_by_key("one", "1".to_string(), 5).unwrap();
3186 region.upsert_by_key("two", "2".to_string(), 5).unwrap();
3187 region.upsert_by_key("three", "3".to_string(), 5).unwrap();
3188
3189 let keys = region.keys();
3190 assert_eq!(keys.len(), 3);
3191 assert!(keys.contains(&"one"));
3192 assert!(keys.contains(&"two"));
3193 assert!(keys.contains(&"three"));
3194
3195 region.remove_by_key("two");
3197 let keys = region.keys();
3198 assert_eq!(keys.len(), 2);
3199 assert!(keys.contains(&"one"));
3200 assert!(!keys.contains(&"two"));
3201 assert!(keys.contains(&"three"));
3202 }
3203
3204 #[test]
3205 fn test_region_entry_serialization_with_key_field() {
3206 let entry_with_key = RegionEntry {
3208 content: "some data".to_string(),
3209 tokens: 10,
3210 timestamp: 1234567890,
3211 metadata: None,
3212 kind: EntryKind::default(),
3213 key: Some("mykey".to_string()),
3214 };
3215 let json = serde_json::to_string(&entry_with_key).unwrap();
3216 let deserialized: RegionEntry = serde_json::from_str(&json).unwrap();
3217 assert_eq!(deserialized.key.as_deref(), Some("mykey"));
3218 assert_eq!(deserialized.content, "some data");
3219 assert_eq!(deserialized.tokens, 10);
3220
3221 let entry_no_key = RegionEntry {
3223 content: "no key data".to_string(),
3224 tokens: 7,
3225 timestamp: 1234567890,
3226 metadata: None,
3227 kind: EntryKind::default(),
3228 key: None,
3229 };
3230 let json = serde_json::to_string(&entry_no_key).unwrap();
3231 assert!(!json.contains("\"key\""));
3232 let deserialized: RegionEntry = serde_json::from_str(&json).unwrap();
3233 assert!(deserialized.key.is_none());
3234 assert_eq!(deserialized.content, "no key data");
3235 }
3236
3237 #[test]
3238 fn test_hashmap_partial_eq() {
3239 let a = RegionKind::HashMap {
3240 max_entries: Some(5),
3241 };
3242 let b = RegionKind::HashMap {
3243 max_entries: Some(5),
3244 };
3245 let c = RegionKind::HashMap {
3246 max_entries: Some(10),
3247 };
3248 let d = RegionKind::HashMap { max_entries: None };
3249
3250 assert_eq!(a, b);
3251 assert_ne!(a, c);
3252 assert_ne!(a, d);
3253 assert_ne!(c, d);
3254 assert_ne!(a, RegionKind::Pinned);
3255 assert_ne!(a, RegionKind::Temporary);
3256 }
3257
3258 #[test]
3259 fn test_hashmap_cache_hint_returns_until_changed() {
3260 let kind = RegionKind::HashMap { max_entries: None };
3261 assert_eq!(kind.cache_hint(), crate::cache::CacheHint::UntilChanged);
3262
3263 let kind_with_max = RegionKind::HashMap {
3264 max_entries: Some(10),
3265 };
3266 assert_eq!(
3267 kind_with_max.cache_hint(),
3268 crate::cache::CacheHint::UntilChanged
3269 );
3270 }
3271
3272 #[test]
3275 fn test_remove_by_key_recomputes_taint_when_tracking_enabled() {
3276 let mut region = Region::new(
3279 "kv".to_string(),
3280 RegionKind::HashMap { max_entries: None },
3281 10_000,
3282 )
3283 .with_taint_tracking();
3284 region
3285 .upsert_by_key("k1", "value one".to_string(), 10)
3286 .unwrap();
3287 region
3288 .upsert_by_key("k2", "value two".to_string(), 10)
3289 .unwrap();
3290
3291 assert!(region.remove_by_key("k1"));
3292 assert!(!region.remove_by_key("missing"));
3293 assert_eq!(region.entry_count(), 1);
3294 assert_eq!(region.current_tokens, 10);
3295 }
3296
3297 #[test]
3298 fn test_evict_lru_entry_runs_taint_fixup() {
3299 let mut region = Region::new(
3303 "kv".to_string(),
3304 RegionKind::HashMap {
3305 max_entries: Some(1),
3306 },
3307 10_000,
3308 )
3309 .with_taint_tracking();
3310 region
3311 .upsert_by_key("first", "aaa".to_string(), 10)
3312 .unwrap();
3313 region
3314 .upsert_by_key("second", "bbb".to_string(), 10)
3315 .unwrap();
3316
3317 assert_eq!(region.entry_count(), 1);
3319 assert!(region.get_by_key("second").is_some());
3320 assert!(region.get_by_key("first").is_none());
3321 }
3322
3323 #[test]
3324 fn test_evict_lru_entry_on_empty_region_is_noop() {
3325 let mut region = Region::new(
3329 "kv".to_string(),
3330 RegionKind::HashMap {
3331 max_entries: Some(4),
3332 },
3333 1000,
3334 );
3335 assert_eq!(region.entry_count(), 0);
3336 region.evict_lru_entry();
3337 assert_eq!(region.entry_count(), 0);
3338 assert_eq!(region.current_tokens, 0);
3339 }
3340
3341 #[test]
3345 fn a_keyed_entry_can_be_added_to_any_region_kind_and_found_again() {
3346 for kind in [
3347 RegionKind::Temporary,
3348 RegionKind::Clearable,
3349 RegionKind::Pinned,
3350 ] {
3351 let mut region = Region::new("r".to_string(), kind.clone(), 1000);
3352 region
3353 .add_keyed_entry("doc", "body".to_string(), 10)
3354 .unwrap();
3355 assert_eq!(
3356 region.get_by_key("doc").map(|e| e.content.as_str()),
3357 Some("body"),
3358 "{kind:?}"
3359 );
3360 assert!(region.remove_by_key("doc"), "{kind:?}");
3361 assert_eq!(region.current_tokens, 0, "{kind:?}");
3362 }
3363 }
3364
3365 #[test]
3369 fn appending_under_one_key_twice_keeps_both_entries() {
3370 let mut region = Region::new("r".to_string(), RegionKind::Temporary, 1000);
3371 region
3372 .add_keyed_entry("doc", "first".to_string(), 5)
3373 .unwrap();
3374 region
3375 .add_keyed_entry("doc", "second".to_string(), 5)
3376 .unwrap();
3377 assert_eq!(region.content.len(), 2);
3378 assert_eq!(region.current_tokens, 10);
3379 }
3380
3381 #[test]
3384 fn a_refused_keyed_write_adds_nothing() {
3385 let mut region = Region::new("r".to_string(), RegionKind::Temporary, 10);
3386 assert!(
3387 region
3388 .add_keyed_entry("doc", "too big".to_string(), 99)
3389 .is_err()
3390 );
3391 assert!(region.content.is_empty());
3392 assert_eq!(region.current_tokens, 0);
3393 }
3394
3395 #[test]
3398 fn remove_at_releases_by_position_and_reports_a_miss() {
3399 let mut region = Region::new("r".to_string(), RegionKind::Temporary, 1000);
3400 for text in ["a", "b", "c"] {
3401 region.add_entry(text.to_string(), 5).unwrap();
3402 }
3403 assert!(region.remove_at(1));
3404 assert_eq!(region.current_tokens, 10);
3405 let left: Vec<_> = region.content.iter().map(|e| e.content.as_str()).collect();
3406 assert_eq!(left, vec!["a", "c"]);
3407
3408 assert!(!region.remove_at(9), "nothing at that position");
3409 assert_eq!(region.content.len(), 2, "a miss changes nothing");
3410 }
3411
3412 #[test]
3415 fn release_oldest_takes_what_it_can_and_says_how_much() {
3416 let mut region = Region::new("r".to_string(), RegionKind::Temporary, 1000);
3417 for text in ["a", "b", "c"] {
3418 region.add_entry(text.to_string(), 5).unwrap();
3419 }
3420 assert_eq!(region.release_oldest(2), 2);
3421 assert_eq!(
3422 region.content.first().map(|e| e.content.as_str()),
3423 Some("c"),
3424 "the oldest two went"
3425 );
3426 assert_eq!(region.release_oldest(10), 1, "only one was left");
3427 assert_eq!(region.release_oldest(3), 0, "and now none");
3428 assert_eq!(region.current_tokens, 0);
3429 }
3430
3431 #[test]
3435 fn a_reject_region_distinguishes_being_full_from_an_oversized_write() {
3436 let mut region = Region::new("r".to_string(), RegionKind::Temporary, 100);
3437 region.admission = Admission::Reject;
3438
3439 let err = region
3445 .add_entry("huge".to_string(), 500)
3446 .unwrap_err()
3447 .to_string();
3448 assert!(err.contains("exceeds token budget"), "{err}");
3449
3450 region.add_entry("fits".to_string(), 90).unwrap();
3451 let err = region
3452 .add_entry("more".to_string(), 50)
3453 .unwrap_err()
3454 .to_string();
3455 assert!(err.contains("Region 'r' is full"), "{err}");
3456 assert!(err.contains("90/100 tokens"), "{err}");
3457 assert!(err.contains("release an entry"), "says what to do: {err}");
3458 }
3459
3460 #[test]
3465 fn a_reject_sliding_window_refuses_rather_than_rolling_off() {
3466 let mut region = Region::new(
3467 "r".to_string(),
3468 RegionKind::SlidingWindow {
3469 max_items: 2,
3470 eviction_strategy: EvictionStrategy::PerItem,
3471 },
3472 1000,
3473 );
3474 region.admission = Admission::Reject;
3475 region.add_entry("one".to_string(), 5).unwrap();
3476 region.add_entry("two".to_string(), 5).unwrap();
3477
3478 let err = region
3479 .add_entry("three".to_string(), 5)
3480 .unwrap_err()
3481 .to_string();
3482 assert!(err.contains("is full"), "{err}");
3483 assert_eq!(region.content.len(), 2);
3484 assert_eq!(
3485 region.content.first().map(|e| e.content.as_str()),
3486 Some("one"),
3487 "the oldest survived"
3488 );
3489
3490 let mut evicting = Region::new(
3493 "r".to_string(),
3494 RegionKind::SlidingWindow {
3495 max_items: 2,
3496 eviction_strategy: EvictionStrategy::PerItem,
3497 },
3498 1000,
3499 );
3500 for text in ["one", "two", "three"] {
3501 evicting.add_entry(text.to_string(), 5).unwrap();
3502 }
3503 assert_eq!(evicting.content.len(), 2);
3504 assert_eq!(
3505 evicting.content.first().map(|e| e.content.as_str()),
3506 Some("two"),
3507 "the oldest rolled off as it always did"
3508 );
3509 }
3510}