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 #[serde(default)]
468 pub volatility: Volatility,
469
470 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub description: Option<String>,
478
479 #[serde(default)]
489 pub describe_in_prompt: bool,
490}
491
492#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
505#[serde(rename_all = "snake_case")]
506pub enum Admission {
507 #[default]
510 Evict,
511 Reject,
514}
515
516#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
534#[serde(rename_all = "snake_case")]
535pub enum Volatility {
536 Stable,
540 Grows,
545 #[default]
553 Rewritten,
554}
555
556mod schema;
557
558pub use schema::{ContentFormat, RegionSchema, Validator};
559
560pub(crate) fn default_true() -> bool {
562 true
563}
564
565impl Region {
566 pub fn new(name: String, kind: RegionKind, max_tokens: usize) -> Self {
568 Self {
569 name,
570 kind,
571 content: Vec::new(),
572 max_tokens,
573 current_tokens: 0,
574 schema: None,
575 taint: None,
576 needs_message_compaction: false,
577 summarizable: true,
578 admission: Admission::default(),
579 volatility: Volatility::default(),
580 description: None,
581 describe_in_prompt: false,
582 }
583 }
584
585 pub fn with_taint_tracking(mut self) -> Self {
587 self.taint = Some(crate::taint::RegionTaint::new());
588 self
589 }
590
591 pub fn enable_taint_tracking(&mut self) {
593 if self.taint.is_none() {
594 self.taint = Some(crate::taint::RegionTaint::new());
595 }
596 }
597
598 pub fn taint_level(&self) -> Option<crate::taint::TaintLevel> {
600 self.taint.as_ref().map(|t| t.level())
601 }
602
603 fn push_entry(
616 &mut self,
617 content: String,
618 tokens: usize,
619 metadata: Option<serde_json::Value>,
620 kind: EntryKind,
621 taint_level: crate::taint::TaintLevel,
622 key: Option<&str>,
623 ) -> crate::error::Result<()> {
624 if let Some(schema) = &self.schema {
625 schema.validate(&content)?;
626 }
627
628 if self.current_tokens + tokens > self.max_tokens {
629 if self.admission == Admission::Reject && !self.content.is_empty() {
635 return Err(crate::error::Error::RegionFull {
636 region: self.name.clone(),
637 used: self.current_tokens,
638 max: self.max_tokens,
639 });
640 }
641 return Err(crate::error::Error::TokenBudgetExceeded {
642 used: self.current_tokens + tokens,
643 max: self.max_tokens,
644 });
645 }
646 if self.admission == Admission::Reject && self.would_roll_off() {
650 return Err(crate::error::Error::RegionFull {
651 region: self.name.clone(),
652 used: self.current_tokens,
653 max: self.max_tokens,
654 });
655 }
656
657 self.content.push(RegionEntry {
658 content,
659 tokens,
660 timestamp: chrono::Utc::now().timestamp(),
661 metadata,
662 kind,
663 key: key.map(str::to_string),
664 });
665 self.current_tokens += tokens;
666
667 if let Some(taint) = &mut self.taint {
671 taint.add_entry(taint_level);
672 }
673
674 self.enforce_sliding_window();
675
676 Ok(())
677 }
678
679 fn would_roll_off(&self) -> bool {
684 match &self.kind {
685 RegionKind::SlidingWindow { max_items, .. } => self.content.len() + 1 > *max_items,
686 _ => false,
687 }
688 }
689
690 pub fn add_keyed_entry(
696 &mut self,
697 key: &str,
698 content: String,
699 tokens: usize,
700 ) -> crate::error::Result<()> {
701 self.push_entry(
702 content,
703 tokens,
704 None,
705 EntryKind::default(),
706 crate::taint::TaintLevel::Public,
707 Some(key),
708 )
709 }
710
711 pub fn remove_at(&mut self, index: usize) -> bool {
717 if index >= self.content.len() {
718 return false;
719 }
720 let entry = self.content.remove(index);
721 self.current_tokens = self.current_tokens.saturating_sub(entry.tokens);
722 true
723 }
724
725 pub fn release_oldest(&mut self, n: usize) -> usize {
730 let count = n.min(self.content.len());
731 for _ in 0..count {
732 self.remove_oldest();
733 }
734 count
735 }
736
737 pub fn add_tainted_entry(
739 &mut self,
740 content: String,
741 tokens: usize,
742 taint_level: crate::taint::TaintLevel,
743 ) -> crate::error::Result<()> {
744 self.push_entry(
745 content,
746 tokens,
747 None,
748 EntryKind::default(),
749 taint_level,
750 None,
751 )
752 }
753
754 pub fn add_typed_tainted_entry(
763 &mut self,
764 content: String,
765 tokens: usize,
766 kind: EntryKind,
767 taint_level: crate::taint::TaintLevel,
768 ) -> crate::error::Result<()> {
769 self.push_entry(content, tokens, None, kind, taint_level, None)
770 }
771
772 pub fn with_schema(mut self, schema: RegionSchema) -> Self {
774 self.schema = Some(schema);
775 self
776 }
777
778 pub fn add_entry(&mut self, content: String, tokens: usize) -> crate::error::Result<()> {
783 self.push_entry(
784 content,
785 tokens,
786 None,
787 EntryKind::default(),
788 crate::taint::TaintLevel::Public,
789 None,
790 )
791 }
792
793 pub fn add_entry_with_metadata(
795 &mut self,
796 content: String,
797 tokens: usize,
798 metadata: serde_json::Value,
799 ) -> crate::error::Result<()> {
800 self.push_entry(
801 content,
802 tokens,
803 Some(metadata),
804 EntryKind::default(),
805 crate::taint::TaintLevel::Public,
806 None,
807 )
808 }
809
810 pub fn add_typed_entry(
816 &mut self,
817 content: String,
818 tokens: usize,
819 kind: EntryKind,
820 ) -> crate::error::Result<()> {
821 self.push_entry(
822 content,
823 tokens,
824 None,
825 kind,
826 crate::taint::TaintLevel::Public,
827 None,
828 )
829 }
830
831 pub fn carry_entry(&mut self, entry: RegionEntry) -> crate::error::Result<()> {
845 if self.current_tokens + entry.tokens > self.max_tokens {
847 return Err(crate::error::Error::TokenBudgetExceeded {
848 used: self.current_tokens + entry.tokens,
849 max: self.max_tokens,
850 });
851 }
852
853 self.current_tokens += entry.tokens;
854 self.content.push(entry);
855
856 self.enforce_sliding_window();
858
859 Ok(())
860 }
861
862 pub fn upsert_by_key(
865 &mut self,
866 key: &str,
867 content: String,
868 tokens: usize,
869 ) -> Result<(), String> {
870 if let Some(pos) = self
872 .content
873 .iter()
874 .position(|e| e.key.as_deref() == Some(key))
875 {
876 let old_tokens = self.content[pos].tokens;
877 self.current_tokens -= old_tokens;
878 self.content[pos].content = content;
879 self.content[pos].tokens = tokens;
880 self.content[pos].timestamp = chrono::Utc::now().timestamp();
881 self.current_tokens += tokens;
882 return Ok(());
883 }
884
885 let max_entries = if let RegionKind::HashMap {
887 max_entries: Some(max),
888 } = &self.kind
889 {
890 Some(*max)
891 } else {
892 None
893 };
894 if let Some(max) = max_entries {
895 while self.content.len() >= max {
896 self.evict_lru_entry();
897 }
898 }
899
900 while self.current_tokens + tokens > self.max_tokens && !self.content.is_empty() {
902 self.evict_lru_entry();
903 }
904
905 if self.current_tokens + tokens > self.max_tokens {
906 return Err(format!(
907 "Entry ({} tokens) exceeds region budget ({} max)",
908 tokens, self.max_tokens
909 ));
910 }
911
912 self.content.push(RegionEntry {
913 content,
914 tokens,
915 timestamp: chrono::Utc::now().timestamp(),
916 metadata: None,
917 kind: EntryKind::default(),
918 key: Some(key.to_string()),
919 });
920 self.current_tokens += tokens;
921 Ok(())
922 }
923
924 pub fn get_by_key(&self, key: &str) -> Option<&RegionEntry> {
926 self.content.iter().find(|e| e.key.as_deref() == Some(key))
927 }
928
929 pub fn remove_by_key(&mut self, key: &str) -> bool {
931 if let Some(pos) = self
932 .content
933 .iter()
934 .position(|e| e.key.as_deref() == Some(key))
935 {
936 let tokens = self.content[pos].tokens;
937 self.content.remove(pos);
938 self.current_tokens -= tokens;
939 if let Some(taint) = &mut self.taint {
940 taint.remove_at(pos);
941 }
942 true
943 } else {
944 false
945 }
946 }
947
948 pub fn keys(&self) -> Vec<&str> {
950 self.content
951 .iter()
952 .filter_map(|e| e.key.as_deref())
953 .collect()
954 }
955
956 fn evict_lru_entry(&mut self) {
958 if self.content.is_empty() {
959 return;
960 }
961 let oldest_idx = self
962 .content
963 .iter()
964 .enumerate()
965 .min_by_key(|(_, e)| e.timestamp)
966 .map(|(i, _)| i)
967 .unwrap_or(0);
968 let tokens = self.content[oldest_idx].tokens;
969 self.content.remove(oldest_idx);
970 self.current_tokens -= tokens;
971 if let Some(taint) = &mut self.taint {
972 taint.remove_at(oldest_idx);
973 }
974 }
975
976 fn enforce_sliding_window(&mut self) {
987 if let RegionKind::SlidingWindow {
988 max_items,
989 eviction_strategy,
990 } = &self.kind
991 {
992 let max = *max_items;
993 match eviction_strategy.clone() {
994 EvictionStrategy::PerItem => {
995 while self.content.len() > max && self.remove_oldest().is_some() {}
999 }
1000 EvictionStrategy::Bulk { overflow } => {
1001 if self.content.len() > max + overflow {
1002 while self.content.len() > max && self.remove_oldest().is_some() {}
1003 }
1004 }
1005 EvictionStrategy::Compact { compact_count } => {
1006 if self.content.len() > max + compact_count * 2 {
1007 while self.content.len() > max && self.remove_oldest().is_some() {}
1010 self.needs_message_compaction = false;
1011 } else if self.content.len() > max + compact_count {
1012 self.needs_message_compaction = true;
1013 }
1014 }
1015 }
1016 }
1017 }
1018
1019 fn turn_group_size_at(&self, idx: usize) -> usize {
1027 if idx >= self.content.len() {
1028 return 0;
1029 }
1030 match &self.content[idx].kind {
1031 EntryKind::AssistantTurn { .. } => {
1032 let mut size = 1;
1033 while idx + size < self.content.len() {
1034 if matches!(self.content[idx + size].kind, EntryKind::ToolResult { .. }) {
1035 size += 1;
1036 } else {
1037 break;
1038 }
1039 }
1040 size
1041 }
1042 _ => 1,
1043 }
1044 }
1045
1046 pub fn clear(&mut self) {
1048 self.content.clear();
1049 self.current_tokens = 0;
1050 if let Some(taint) = &mut self.taint {
1051 taint.clear();
1052 }
1053 }
1054
1055 pub fn remove_oldest(&mut self) -> Option<RegionEntry> {
1057 if self.content.is_empty() {
1058 return None;
1059 }
1060 let group_size = self.turn_group_size_at(0);
1064 let mut first = None;
1065 let mut extra_tokens = 0usize;
1066 let mut i = 0;
1069 while i < group_size && !self.content.is_empty() {
1070 let entry_tokens = self.content[0].tokens;
1071 self.current_tokens -= entry_tokens;
1072 let removed = self.content.remove(0);
1073 if let Some(taint) = &mut self.taint {
1074 taint.remove_oldest();
1075 }
1076 if i == 0 {
1077 first = Some(removed);
1078 } else {
1079 extra_tokens += entry_tokens;
1080 }
1081 i += 1;
1082 }
1083 first.map(|mut entry| {
1089 entry.tokens += extra_tokens;
1090 entry
1091 })
1092 }
1093
1094 pub fn remove_entries_by_prefix(&mut self, prefix: &str) {
1100 let mut i = 0;
1101 while i < self.content.len() {
1102 if self.content[i].content.starts_with(prefix) {
1103 let tokens = self.content[i].tokens;
1104 self.content.remove(i);
1105 self.current_tokens -= tokens;
1106 if let Some(taint) = &mut self.taint {
1107 taint.remove_at(i);
1108 }
1109 } else {
1110 i += 1;
1111 }
1112 }
1113 }
1114
1115 pub fn entry_count(&self) -> usize {
1117 self.content.len()
1118 }
1119
1120 pub fn needs_compaction(&self) -> bool {
1122 if let RegionKind::Compacting { threshold_tokens } = self.kind {
1123 self.current_tokens > threshold_tokens
1124 } else {
1125 false
1126 }
1127 }
1128}
1129
1130#[derive(Debug, Clone, Serialize, Deserialize)]
1134pub struct RegionEntry {
1135 pub content: String,
1137
1138 pub tokens: usize,
1140
1141 pub timestamp: i64,
1143
1144 pub metadata: Option<serde_json::Value>,
1146
1147 #[serde(default)]
1151 pub kind: EntryKind,
1152
1153 #[serde(default, skip_serializing_if = "Option::is_none")]
1155 pub key: Option<String>,
1156}
1157
1158#[cfg(test)]
1161mod tests {
1162 use super::*;
1163
1164 fn checklist() -> Region {
1167 Region::new("todos".to_string(), RegionKind::Checklist, 10_000)
1168 }
1169
1170 #[test]
1176 fn a_malformed_entry_is_not_an_item() {
1177 let mut r = checklist();
1178 r.add_entry("a plain note".to_string(), 3).unwrap();
1180 r.add_entry_with_metadata(
1182 "something else".to_string(),
1183 3,
1184 serde_json::json!({ "unrelated": true }),
1185 )
1186 .unwrap();
1187 r.add_entry_with_metadata(
1189 "bad id".to_string(),
1190 3,
1191 serde_json::json!({ "checklist_id": "one" }),
1192 )
1193 .unwrap();
1194
1195 assert!(r.checklist_items().is_empty(), "none of those are items");
1196 assert!(r.open_checklist_items().is_empty());
1197 assert!(
1198 r.render_checklist().is_empty(),
1199 "and they do not render as a checklist"
1200 );
1201 }
1202
1203 #[test]
1206 fn a_checklist_caches_until_it_changes() {
1207 assert_eq!(
1208 RegionKind::Checklist.cache_hint(),
1209 crate::cache::CacheHint::UntilChanged
1210 );
1211 }
1212
1213 #[test]
1214 fn a_note_appears_in_the_render() {
1215 let mut r = checklist();
1216 let id = r.add_checklist_item("blocked".to_string(), 2).unwrap();
1217 r.note_checklist_item(id, "waiting on the manual");
1218 let rendered = r.render_checklist();
1219 assert!(
1220 rendered.contains("note: waiting on the manual"),
1221 "{rendered}"
1222 );
1223 }
1224
1225 #[test]
1228 fn an_item_over_budget_is_refused() {
1229 let mut r = Region::new("todos".to_string(), RegionKind::Checklist, 4);
1230 assert!(r.add_checklist_item("x".to_string(), 99).is_err());
1231 assert!(r.checklist_items().is_empty());
1232 }
1233
1234 #[test]
1235 fn an_added_item_starts_open_and_gets_an_id() {
1236 let mut r = checklist();
1237 let first = r
1238 .add_checklist_item("compute the fee table".to_string(), 5)
1239 .unwrap();
1240 let second = r
1241 .add_checklist_item("check the manual".to_string(), 5)
1242 .unwrap();
1243 assert_eq!((first, second), (1, 2), "ids are stable and sequential");
1244 assert_eq!(r.open_checklist_items().len(), 2);
1245 }
1246
1247 #[test]
1248 fn completing_an_item_closes_it_and_nothing_else() {
1249 let mut r = checklist();
1250 let id = r.add_checklist_item("one".to_string(), 2).unwrap();
1251 r.add_checklist_item("two".to_string(), 2).unwrap();
1252
1253 assert!(r.complete_checklist_item(id));
1254 let open = r.open_checklist_items();
1255 assert_eq!(open.len(), 1);
1256 assert_eq!(open[0].text, "two");
1257 assert_eq!(
1258 r.checklist_items().len(),
1259 2,
1260 "done items are kept, not deleted"
1261 );
1262 }
1263
1264 #[test]
1265 fn an_unknown_id_reports_failure_rather_than_ticking_something_else() {
1266 let mut r = checklist();
1269 r.add_checklist_item("one".to_string(), 2).unwrap();
1270 assert!(!r.complete_checklist_item(99));
1271 assert!(!r.note_checklist_item(99, "x"));
1272 assert_eq!(r.open_checklist_items().len(), 1);
1273 }
1274
1275 #[test]
1276 fn a_note_records_without_closing() {
1277 let mut r = checklist();
1278 let id = r
1279 .add_checklist_item("blocked thing".to_string(), 2)
1280 .unwrap();
1281 assert!(r.note_checklist_item(id, "waiting on the manual"));
1282 let item = &r.checklist_items()[0];
1283 assert!(!item.done, "a note is not a completion");
1284 assert_eq!(item.note.as_deref(), Some("waiting on the manual"));
1285 }
1286
1287 #[test]
1290 fn the_render_puts_open_items_first() {
1291 let mut r = checklist();
1292 let done = r
1293 .add_checklist_item("already finished".to_string(), 2)
1294 .unwrap();
1295 r.add_checklist_item("still to do".to_string(), 2).unwrap();
1296 r.complete_checklist_item(done);
1297
1298 let rendered = r.render_checklist();
1299 let open_at = rendered.find("still to do").expect("open item rendered");
1300 let done_at = rendered
1301 .find("already finished")
1302 .expect("done item rendered");
1303 assert!(open_at < done_at, "open before done:\n{rendered}");
1304 assert!(rendered.contains("1 open, 1 done"), "{rendered}");
1305 assert!(
1306 rendered.contains("[x]") && rendered.contains("[ ]"),
1307 "{rendered}"
1308 );
1309 }
1310
1311 #[test]
1312 fn an_empty_checklist_renders_nothing() {
1313 assert!(checklist().render_checklist().is_empty());
1315 }
1316
1317 #[test]
1320 fn ids_do_not_get_reused_after_a_drop() {
1321 let mut r = checklist();
1322 r.add_checklist_item("one".to_string(), 2).unwrap();
1323 let second = r.add_checklist_item("two".to_string(), 2).unwrap();
1324 r.content.remove(0);
1325 let third = r.add_checklist_item("three".to_string(), 2).unwrap();
1326 assert!(third > second, "a reused id would tick off the wrong item");
1327 }
1328
1329 #[test]
1330 fn test_region_creation() {
1331 let region = Region::new("test".to_string(), RegionKind::Pinned, 1000);
1332 assert_eq!(region.name, "test");
1333 assert_eq!(region.max_tokens, 1000);
1334 assert_eq!(region.current_tokens, 0);
1335 }
1336
1337 #[test]
1338 fn test_sliding_window_config() {
1339 let kind = RegionKind::SlidingWindow {
1340 max_items: 10,
1341 eviction_strategy: EvictionStrategy::PerItem,
1342 };
1343 let region = Region::new("history".to_string(), kind.clone(), 5000);
1344 assert_eq!(region.kind, kind);
1345 }
1346
1347 #[test]
1348 fn test_region_kind_equality() {
1349 assert_eq!(RegionKind::Clearable, RegionKind::Clearable);
1350 assert_eq!(
1351 RegionKind::Compacting {
1352 threshold_tokens: 500
1353 },
1354 RegionKind::Compacting {
1355 threshold_tokens: 500
1356 }
1357 );
1358 assert_eq!(
1359 RegionKind::CompactHistory {
1360 source_region: "conv".to_string()
1361 },
1362 RegionKind::CompactHistory {
1363 source_region: "conv".to_string()
1364 }
1365 );
1366 assert_ne!(RegionKind::Pinned, RegionKind::Temporary);
1367 }
1368
1369 #[test]
1370 fn custom_kind_equality_compares_script_and_persistent() {
1371 let a = RegionKind::Custom {
1372 script: "conv.rhai".to_string(),
1373 persistent: false,
1374 };
1375 assert_eq!(a, a.clone());
1376 assert_ne!(
1377 a,
1378 RegionKind::Custom {
1379 script: "other.rhai".to_string(),
1380 persistent: false,
1381 }
1382 );
1383 assert_ne!(
1384 a,
1385 RegionKind::Custom {
1386 script: "conv.rhai".to_string(),
1387 persistent: true,
1388 }
1389 );
1390 assert_ne!(a, RegionKind::Temporary);
1391 }
1392
1393 #[test]
1394 fn custom_kind_serde_round_trips() {
1395 let kind = RegionKind::Custom {
1396 script: "hooks/conv.rhai".to_string(),
1397 persistent: true,
1398 };
1399 let json = serde_json::to_string(&kind).unwrap();
1400 let back: RegionKind = serde_json::from_str(&json).unwrap();
1401 assert_eq!(kind, back);
1402 let old: RegionKind = serde_json::from_str("\"Pinned\"").unwrap();
1404 assert_eq!(old, RegionKind::Pinned);
1405 }
1406
1407 #[test]
1408 fn custom_kind_cache_hint_follows_persistent() {
1409 assert_eq!(
1410 RegionKind::Custom {
1411 script: "s.rhai".to_string(),
1412 persistent: true,
1413 }
1414 .cache_hint(),
1415 crate::cache::CacheHint::Always
1416 );
1417 assert_eq!(
1418 RegionKind::Custom {
1419 script: "s.rhai".to_string(),
1420 persistent: false,
1421 }
1422 .cache_hint(),
1423 crate::cache::CacheHint::UntilChanged
1424 );
1425 }
1426
1427 #[test]
1428 fn carry_entry_preserves_kind_metadata_key_and_timestamp() {
1429 let mut source = Region::new("conversation".to_string(), RegionKind::Temporary, 10_000);
1430 source
1431 .add_typed_entry(
1432 "result body".to_string(),
1433 10,
1434 EntryKind::ToolResult {
1435 tool_call_id: "call_1".to_string(),
1436 tool_name: "read_file".to_string(),
1437 is_error: false,
1438 },
1439 )
1440 .unwrap();
1441 let mut entry = source.content[0].clone();
1442 entry.metadata = Some(serde_json::json!({"origin": "test"}));
1443 entry.key = Some("k".to_string());
1444 let stamped = entry.timestamp;
1445
1446 let mut dest = Region::new("conversation".to_string(), RegionKind::Temporary, 10_000);
1447 dest.carry_entry(entry).unwrap();
1448
1449 let carried = &dest.content[0];
1450 assert!(matches!(
1451 &carried.kind,
1452 EntryKind::ToolResult { tool_call_id, .. } if tool_call_id == "call_1"
1453 ));
1454 assert_eq!(
1455 carried.metadata,
1456 Some(serde_json::json!({"origin": "test"}))
1457 );
1458 assert_eq!(carried.key.as_deref(), Some("k"));
1459 assert_eq!(carried.timestamp, stamped);
1460 assert_eq!(dest.current_tokens, 10);
1461 }
1462
1463 #[test]
1464 fn carry_entry_rejects_over_budget() {
1465 let mut dest = Region::new("small".to_string(), RegionKind::Temporary, 5);
1466 let mut source = Region::new("src".to_string(), RegionKind::Temporary, 100);
1467 source.add_entry("filler".to_string(), 10).unwrap();
1468 let err = dest.carry_entry(source.content[0].clone()).unwrap_err();
1469 assert_eq!(err.to_string(), "Content exceeds token budget: 10 > 5");
1470 assert!(dest.content.is_empty());
1471 assert_eq!(dest.current_tokens, 0);
1472 }
1473
1474 #[test]
1475 fn carry_entry_enforces_sliding_window_max_items() {
1476 let mut source = Region::new("src".to_string(), RegionKind::Temporary, 10_000);
1477 for i in 0..4 {
1478 source.add_entry(format!("msg{i}"), 10).unwrap();
1479 }
1480 let mut dest = Region::new(
1481 "conv".to_string(),
1482 RegionKind::SlidingWindow {
1483 max_items: 3,
1484 eviction_strategy: EvictionStrategy::PerItem,
1485 },
1486 10_000,
1487 );
1488 for entry in &source.content {
1489 dest.carry_entry(entry.clone()).unwrap();
1490 }
1491 assert_eq!(dest.content.len(), 3);
1492 assert_eq!(dest.content[0].content, "msg1");
1493 }
1494
1495 #[test]
1496 fn test_sliding_window_enforces_max_items() {
1497 let mut region = Region::new(
1498 "conv".to_string(),
1499 RegionKind::SlidingWindow {
1500 max_items: 3,
1501 eviction_strategy: EvictionStrategy::PerItem,
1502 },
1503 50000,
1504 );
1505
1506 region.add_entry("msg1".to_string(), 10).unwrap();
1507 region.add_entry("msg2".to_string(), 20).unwrap();
1508 region.add_entry("msg3".to_string(), 30).unwrap();
1509 assert_eq!(region.entry_count(), 3);
1510 assert_eq!(region.current_tokens, 60);
1511
1512 region.add_entry("msg4".to_string(), 40).unwrap();
1514 assert_eq!(region.entry_count(), 3);
1515 assert_eq!(region.content[0].content, "msg2");
1516 assert_eq!(region.content[2].content, "msg4");
1517 assert_eq!(region.current_tokens, 90); region.add_entry("msg5".to_string(), 50).unwrap();
1521 assert_eq!(region.entry_count(), 3);
1522 assert_eq!(region.content[0].content, "msg3");
1523 assert_eq!(region.current_tokens, 120); }
1525
1526 #[test]
1527 fn test_sliding_window_enforces_max_items_with_metadata() {
1528 let mut region = Region::new(
1529 "conv".to_string(),
1530 RegionKind::SlidingWindow {
1531 max_items: 2,
1532 eviction_strategy: EvictionStrategy::PerItem,
1533 },
1534 50000,
1535 );
1536
1537 region
1538 .add_entry_with_metadata("a".to_string(), 10, serde_json::json!({"idx": 1}))
1539 .unwrap();
1540 region
1541 .add_entry_with_metadata("b".to_string(), 20, serde_json::json!({"idx": 2}))
1542 .unwrap();
1543 region
1544 .add_entry_with_metadata("c".to_string(), 30, serde_json::json!({"idx": 3}))
1545 .unwrap();
1546
1547 assert_eq!(region.entry_count(), 2);
1548 assert_eq!(region.content[0].content, "b");
1549 assert_eq!(region.content[1].content, "c");
1550 assert_eq!(region.current_tokens, 50);
1551 }
1552
1553 #[test]
1554 fn test_cache_hint_pinned() {
1555 let kind = RegionKind::Pinned;
1556 assert_eq!(kind.cache_hint(), crate::cache::CacheHint::Always);
1557 }
1558
1559 #[test]
1560 fn test_cache_hint_compact_history() {
1561 let kind = RegionKind::CompactHistory {
1562 source_region: "conv".to_string(),
1563 };
1564 assert_eq!(kind.cache_hint(), crate::cache::CacheHint::Always);
1565 }
1566
1567 #[test]
1568 fn test_cache_hint_compacting() {
1569 let kind = RegionKind::Compacting {
1570 threshold_tokens: 1000,
1571 };
1572 assert_eq!(kind.cache_hint(), crate::cache::CacheHint::UntilChanged);
1573 }
1574
1575 #[test]
1576 fn test_cache_hint_sliding_window() {
1577 let kind = RegionKind::SlidingWindow {
1578 max_items: 10,
1579 eviction_strategy: EvictionStrategy::PerItem,
1580 };
1581 assert_eq!(
1582 kind.cache_hint(),
1583 crate::cache::CacheHint::SlidingPrefix {
1584 stable_fraction: 0.75
1585 }
1586 );
1587 }
1588
1589 #[test]
1590 fn test_cache_hint_temporary() {
1591 assert_eq!(
1592 RegionKind::Temporary.cache_hint(),
1593 crate::cache::CacheHint::Never
1594 );
1595 }
1596
1597 #[test]
1598 fn test_cache_hint_clearable() {
1599 assert_eq!(
1600 RegionKind::Clearable.cache_hint(),
1601 crate::cache::CacheHint::Never
1602 );
1603 }
1604
1605 #[test]
1608 fn test_with_schema_attaches_schema() {
1609 let schema = RegionSchema::new(ContentFormat::Json);
1610 let region =
1611 Region::new("data".to_string(), RegionKind::Temporary, 1000).with_schema(schema);
1612 assert!(region.schema.is_some());
1613 }
1614
1615 #[test]
1616 fn test_add_entry_rejects_content_failing_schema() {
1617 let schema = RegionSchema::new(ContentFormat::Json);
1618 let mut region =
1619 Region::new("data".to_string(), RegionKind::Temporary, 1000).with_schema(schema);
1620 let result = region.add_entry("not json".to_string(), 10);
1621 assert!(result.is_err());
1622 assert_eq!(region.entry_count(), 0);
1623 }
1624
1625 #[test]
1626 fn test_add_entry_accepts_content_passing_schema() {
1627 let schema = RegionSchema::new(ContentFormat::Json);
1628 let mut region =
1629 Region::new("data".to_string(), RegionKind::Temporary, 1000).with_schema(schema);
1630 let result = region.add_entry("{\"a\":1}".to_string(), 10);
1631 assert!(result.is_ok());
1632 assert_eq!(region.entry_count(), 1);
1633 }
1634
1635 #[test]
1636 fn test_add_entry_rejects_over_budget() {
1637 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 10);
1638 let result = region.add_entry("too much".to_string(), 20);
1639 assert_eq!(
1640 result.unwrap_err().to_string(),
1641 "Content exceeds token budget: 20 > 10"
1642 );
1643 assert_eq!(region.entry_count(), 0);
1644 }
1645
1646 #[test]
1647 fn test_add_entry_with_metadata_rejects_content_failing_schema() {
1648 let schema = RegionSchema::new(ContentFormat::Json);
1649 let mut region =
1650 Region::new("data".to_string(), RegionKind::Temporary, 1000).with_schema(schema);
1651 let result =
1652 region.add_entry_with_metadata("not json".to_string(), 10, serde_json::json!({}));
1653 assert!(result.is_err());
1654 }
1655
1656 #[test]
1657 fn test_add_entry_with_metadata_rejects_over_budget() {
1658 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 10);
1659 let result =
1660 region.add_entry_with_metadata("too much".to_string(), 20, serde_json::json!({}));
1661 assert_eq!(
1662 result.unwrap_err().to_string(),
1663 "Content exceeds token budget: 20 > 10"
1664 );
1665 }
1666
1667 #[test]
1668 fn test_add_entry_with_metadata_stores_metadata() {
1669 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
1670 region
1671 .add_entry_with_metadata("hello".to_string(), 5, serde_json::json!({"k": "v"}))
1672 .unwrap();
1673 assert_eq!(
1674 region.content[0].metadata,
1675 Some(serde_json::json!({"k": "v"}))
1676 );
1677 }
1678
1679 #[test]
1682 fn test_clear_removes_all_content_and_resets_tokens() {
1683 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
1684 region.add_entry("a".to_string(), 10).unwrap();
1685 region.add_entry("b".to_string(), 20).unwrap();
1686 assert_eq!(region.entry_count(), 2);
1687
1688 region.clear();
1689 assert_eq!(region.entry_count(), 0);
1690 assert_eq!(region.current_tokens, 0);
1691 }
1692
1693 #[test]
1694 fn test_remove_oldest_returns_and_removes_first_entry() {
1695 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
1696 region.add_entry("first".to_string(), 10).unwrap();
1697 region.add_entry("second".to_string(), 20).unwrap();
1698
1699 let removed = region.remove_oldest().unwrap();
1700 assert_eq!(removed.content, "first");
1701 assert_eq!(region.entry_count(), 1);
1702 assert_eq!(region.current_tokens, 20);
1703 }
1704
1705 #[test]
1706 fn test_remove_oldest_returns_none_when_empty() {
1707 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
1708 assert!(region.remove_oldest().is_none());
1709 }
1710
1711 #[test]
1712 fn test_needs_compaction_true_when_over_threshold() {
1713 let mut region = Region::new(
1714 "impl".to_string(),
1715 RegionKind::Compacting {
1716 threshold_tokens: 10,
1717 },
1718 1000,
1719 );
1720 region.add_entry("x".to_string(), 20).unwrap();
1721 assert!(region.needs_compaction());
1722 }
1723
1724 #[test]
1725 fn test_needs_compaction_false_when_under_threshold() {
1726 let mut region = Region::new(
1727 "impl".to_string(),
1728 RegionKind::Compacting {
1729 threshold_tokens: 100,
1730 },
1731 1000,
1732 );
1733 region.add_entry("x".to_string(), 20).unwrap();
1734 assert!(!region.needs_compaction());
1735 }
1736
1737 #[test]
1738 fn test_needs_compaction_false_for_non_compacting_kind() {
1739 let region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
1740 assert!(!region.needs_compaction());
1741 }
1742
1743 #[test]
1746 fn test_region_schema_with_custom_script() {
1747 let schema = RegionSchema::new(ContentFormat::Custom {
1748 format_name: "special".to_string(),
1749 })
1750 .with_custom_script("validate_special()".to_string());
1751 assert_eq!(schema.custom_script.as_deref(), Some("validate_special()"));
1752 }
1753
1754 #[test]
1757 fn test_validate_json_valid() {
1758 let schema = RegionSchema::new(ContentFormat::Json);
1759 assert!(schema.validate("{\"a\": 1}").is_ok());
1760 }
1761
1762 #[test]
1763 fn test_validate_json_invalid() {
1764 let schema = RegionSchema::new(ContentFormat::Json);
1765 let err = schema.validate("not json").unwrap_err();
1766 assert!(err.to_string().starts_with("Region validation failed:"));
1767 }
1768
1769 #[test]
1770 fn test_validate_mermaid_valid() {
1771 let schema = RegionSchema::new(ContentFormat::Mermaid);
1772 assert!(schema.validate("graph TD\nA-->B").is_ok());
1773 }
1774
1775 #[test]
1776 fn test_validate_mermaid_all_recognized_diagram_types() {
1777 let schema = RegionSchema::new(ContentFormat::Mermaid);
1778 for kind in [
1779 "graph",
1780 "sequenceDiagram",
1781 "classDiagram",
1782 "stateDiagram",
1783 "erDiagram",
1784 "journey",
1785 "gantt",
1786 "pie",
1787 "flowchart",
1788 ] {
1789 assert!(schema.validate(&format!("{} content", kind)).is_ok());
1790 }
1791 }
1792
1793 #[test]
1794 fn test_validate_mermaid_invalid() {
1795 let schema = RegionSchema::new(ContentFormat::Mermaid);
1796 let err = schema.validate("just some text").unwrap_err();
1797 assert!(err.to_string().starts_with("Region validation failed:"));
1798 }
1799
1800 #[test]
1801 fn test_validate_code_non_empty_is_ok() {
1802 let schema = RegionSchema::new(ContentFormat::Code {
1803 language: "rust".to_string(),
1804 });
1805 assert!(schema.validate("fn main() {}").is_ok());
1806 }
1807
1808 #[test]
1809 fn test_validate_code_empty_is_error() {
1810 let schema = RegionSchema::new(ContentFormat::Code {
1811 language: "rust".to_string(),
1812 });
1813 let err = schema.validate(" ").unwrap_err();
1814 assert!(err.to_string().starts_with("Region validation failed:"));
1815 }
1816
1817 #[test]
1818 fn test_validate_markdown_non_empty_is_ok() {
1819 let schema = RegionSchema::new(ContentFormat::Markdown);
1820 assert!(schema.validate("# Heading").is_ok());
1821 }
1822
1823 #[test]
1824 fn test_validate_markdown_empty_is_error() {
1825 let schema = RegionSchema::new(ContentFormat::Markdown);
1826 let err = schema.validate("").unwrap_err();
1827 assert!(err.to_string().starts_with("Region validation failed:"));
1828 }
1829
1830 #[test]
1831 fn test_validate_text_has_no_restrictions() {
1832 let schema = RegionSchema::new(ContentFormat::Text);
1833 assert!(schema.validate("").is_ok());
1834 assert!(schema.validate("anything at all").is_ok());
1835 }
1836
1837 #[test]
1838 fn test_validate_custom_has_no_restrictions_here() {
1839 let schema = RegionSchema::new(ContentFormat::Custom {
1840 format_name: "special".to_string(),
1841 });
1842 assert!(schema.validate("").is_ok());
1845 assert!(schema.validate("whatever").is_ok());
1846 }
1847
1848 #[test]
1851 fn test_region_schema_clone_preserves_fields() {
1852 let schema = RegionSchema::new(ContentFormat::Text).with_custom_script("s".to_string());
1853 let cloned = schema.clone();
1854 assert_eq!(cloned.custom_script.as_deref(), Some("s"));
1855 assert_eq!(cloned.format, ContentFormat::Text);
1856 }
1857
1858 #[test]
1861 fn test_region_with_taint_tracking() {
1862 let region =
1863 Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
1864 assert!(region.taint.is_some());
1865 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
1866 }
1867
1868 #[test]
1869 fn test_region_without_taint_tracking() {
1870 let region = Region::new("test".to_string(), RegionKind::Temporary, 1000);
1871 assert!(region.taint.is_none());
1872 assert_eq!(region.taint_level(), None);
1873 }
1874
1875 #[test]
1876 fn test_enable_taint_tracking() {
1877 let mut region = Region::new("test".to_string(), RegionKind::Temporary, 1000);
1878 assert!(region.taint.is_none());
1879 region.enable_taint_tracking();
1880 assert!(region.taint.is_some());
1881 region.enable_taint_tracking();
1883 assert!(region.taint.is_some());
1884 }
1885
1886 #[test]
1887 fn test_add_tainted_entry() {
1888 let mut region =
1889 Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
1890 region
1891 .add_tainted_entry(
1892 "secret data".to_string(),
1893 10,
1894 crate::taint::TaintLevel::Private,
1895 )
1896 .unwrap();
1897 assert_eq!(
1898 region.taint_level(),
1899 Some(crate::taint::TaintLevel::Private)
1900 );
1901 assert_eq!(region.entry_count(), 1);
1902 }
1903
1904 #[test]
1905 fn test_add_tainted_entry_validates_schema() {
1906 let mut region = Region::new("test".to_string(), RegionKind::Temporary, 1000)
1907 .with_taint_tracking()
1908 .with_schema(RegionSchema::new(ContentFormat::Json));
1909 let result = region.add_tainted_entry(
1910 "not json".to_string(),
1911 10,
1912 crate::taint::TaintLevel::Internal,
1913 );
1914 assert!(result.is_err());
1915 assert_eq!(region.entry_count(), 0);
1916 }
1917
1918 #[test]
1919 fn test_add_tainted_entry_checks_budget() {
1920 let mut region =
1921 Region::new("test".to_string(), RegionKind::Temporary, 10).with_taint_tracking();
1922 let result = region.add_tainted_entry(
1923 "too much".to_string(),
1924 20,
1925 crate::taint::TaintLevel::Internal,
1926 );
1927 assert!(result.is_err());
1928 }
1929
1930 #[test]
1931 fn test_add_entry_tracks_taint_as_public() {
1932 let mut region =
1933 Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
1934 region.add_entry("public data".to_string(), 10).unwrap();
1935 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
1936 }
1937
1938 #[test]
1939 fn test_taint_recovery_on_remove_oldest() {
1940 let mut region =
1941 Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
1942 region
1943 .add_tainted_entry("private".to_string(), 10, crate::taint::TaintLevel::Private)
1944 .unwrap();
1945 region
1946 .add_tainted_entry("public".to_string(), 10, crate::taint::TaintLevel::Public)
1947 .unwrap();
1948 assert_eq!(
1949 region.taint_level(),
1950 Some(crate::taint::TaintLevel::Private)
1951 );
1952
1953 region.remove_oldest(); assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
1955 }
1956
1957 #[test]
1958 fn test_taint_recovery_on_clear() {
1959 let mut region =
1960 Region::new("test".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
1961 region
1962 .add_tainted_entry("private".to_string(), 10, crate::taint::TaintLevel::Private)
1963 .unwrap();
1964 region.clear();
1965 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
1966 }
1967
1968 #[test]
1969 fn test_taint_recovery_on_sliding_window_eviction() {
1970 let mut region = Region::new(
1971 "conv".to_string(),
1972 RegionKind::SlidingWindow {
1973 max_items: 2,
1974 eviction_strategy: EvictionStrategy::PerItem,
1975 },
1976 50000,
1977 )
1978 .with_taint_tracking();
1979
1980 region
1981 .add_tainted_entry("private".to_string(), 10, crate::taint::TaintLevel::Private)
1982 .unwrap();
1983 region
1984 .add_tainted_entry("public1".to_string(), 10, crate::taint::TaintLevel::Public)
1985 .unwrap();
1986 assert_eq!(
1987 region.taint_level(),
1988 Some(crate::taint::TaintLevel::Private)
1989 );
1990
1991 region
1993 .add_tainted_entry("public2".to_string(), 10, crate::taint::TaintLevel::Public)
1994 .unwrap();
1995 assert_eq!(region.entry_count(), 2);
1996 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
1997 }
1998
1999 #[test]
2000 fn test_taint_field_not_serialized_when_none() {
2001 let region = Region::new("test".to_string(), RegionKind::Temporary, 1000);
2002 let json = serde_json::to_string(®ion).unwrap();
2003 assert!(!json.contains("taint"));
2004 }
2005
2006 #[test]
2007 fn test_taint_field_deserialized_as_none_when_missing() {
2008 let json = r#"{"name":"test","kind":"Temporary","content":[],"max_tokens":1000,"current_tokens":0,"schema":null}"#;
2009 let region: Region = serde_json::from_str(json).unwrap();
2010 assert!(region.taint.is_none());
2011 }
2012
2013 #[test]
2014 fn test_add_typed_tainted_entry() {
2015 let mut region = Region::new(
2016 "conversation".to_string(),
2017 RegionKind::SlidingWindow {
2018 max_items: 100,
2019 eviction_strategy: EvictionStrategy::PerItem,
2020 },
2021 1000,
2022 )
2023 .with_taint_tracking();
2024
2025 region
2026 .add_typed_tainted_entry(
2027 "secret data".to_string(),
2028 10,
2029 EntryKind::ToolResult {
2030 tool_call_id: "tc_1".to_string(),
2031 tool_name: "calendar".to_string(),
2032 is_error: false,
2033 },
2034 crate::taint::TaintLevel::Private,
2035 )
2036 .unwrap();
2037
2038 assert_eq!(region.content.len(), 1);
2039 assert_eq!(
2040 region.content[0].kind,
2041 EntryKind::ToolResult {
2042 tool_call_id: "tc_1".to_string(),
2043 tool_name: "calendar".to_string(),
2044 is_error: false,
2045 }
2046 );
2047 assert_eq!(
2048 region.taint_level(),
2049 Some(crate::taint::TaintLevel::Private)
2050 );
2051 }
2052
2053 #[test]
2057 fn serialized_tool_call_round_trips_thought_signature_and_reads_old_json() {
2058 let with = SerializedToolCall {
2059 id: "c1".into(),
2060 name: "shell".into(),
2061 arguments: serde_json::json!({"command": "ls"}),
2062 thought_signature: Some("sig".into()),
2063 };
2064 let json = serde_json::to_string(&with).unwrap();
2065 let back: SerializedToolCall = serde_json::from_str(&json).unwrap();
2066 assert_eq!(back.thought_signature.as_deref(), Some("sig"));
2067
2068 let old = r#"{"id":"c2","name":"shell","arguments":{}}"#;
2070 let back: SerializedToolCall = serde_json::from_str(old).unwrap();
2071 assert_eq!(back.thought_signature, None);
2072
2073 let without = SerializedToolCall {
2076 id: "c3".into(),
2077 name: "shell".into(),
2078 arguments: serde_json::json!({}),
2079 thought_signature: None,
2080 };
2081 assert!(
2082 !serde_json::to_string(&without)
2083 .unwrap()
2084 .contains("thought_signature")
2085 );
2086 }
2087
2088 #[test]
2089 fn test_add_typed_tainted_entry_checks_budget() {
2090 let mut region = Region::new(
2091 "conversation".to_string(),
2092 RegionKind::SlidingWindow {
2093 max_items: 100,
2094 eviction_strategy: EvictionStrategy::PerItem,
2095 },
2096 5,
2097 )
2098 .with_taint_tracking();
2099
2100 let result = region.add_typed_tainted_entry(
2101 "too large".to_string(),
2102 100,
2103 EntryKind::ToolResult {
2104 tool_call_id: "tc_1".to_string(),
2105 tool_name: "tool".to_string(),
2106 is_error: false,
2107 },
2108 crate::taint::TaintLevel::Internal,
2109 );
2110 assert!(result.is_err());
2111 }
2112
2113 #[test]
2114 fn test_add_typed_tainted_entry_validates_schema() {
2115 let mut region = Region::new("test".to_string(), RegionKind::Pinned, 1000)
2116 .with_taint_tracking()
2117 .with_schema(RegionSchema::new(ContentFormat::Json));
2118
2119 let result = region.add_typed_tainted_entry(
2121 "not json".to_string(),
2122 5,
2123 EntryKind::Text,
2124 crate::taint::TaintLevel::Public,
2125 );
2126 assert!(result.is_err());
2127 }
2128
2129 #[test]
2130 fn test_add_typed_tainted_entry_without_taint_tracking() {
2131 let mut region = Region::new(
2134 "conversation".to_string(),
2135 RegionKind::SlidingWindow {
2136 max_items: 100,
2137 eviction_strategy: EvictionStrategy::PerItem,
2138 },
2139 1000,
2140 );
2141 region
2144 .add_typed_tainted_entry(
2145 "data".to_string(),
2146 10,
2147 EntryKind::Text,
2148 crate::taint::TaintLevel::Private,
2149 )
2150 .unwrap();
2151
2152 assert_eq!(region.content.len(), 1);
2153 assert_eq!(region.taint_level(), None); }
2155
2156 #[test]
2159 fn test_turn_group_size_at_assistant_with_tool_results() {
2160 let mut region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
2161 region
2162 .add_typed_entry(
2163 "assistant response".to_string(),
2164 10,
2165 EntryKind::AssistantTurn {
2166 tool_calls: vec![
2167 SerializedToolCall {
2168 id: "tc_1".to_string(),
2169 name: "read_file".to_string(),
2170 arguments: serde_json::json!({}),
2171 thought_signature: None,
2172 },
2173 SerializedToolCall {
2174 id: "tc_2".to_string(),
2175 name: "write_file".to_string(),
2176 arguments: serde_json::json!({}),
2177 thought_signature: None,
2178 },
2179 ],
2180 },
2181 )
2182 .unwrap();
2183 region
2184 .add_typed_entry(
2185 "result 1".to_string(),
2186 5,
2187 EntryKind::ToolResult {
2188 tool_call_id: "tc_1".to_string(),
2189 tool_name: "read_file".to_string(),
2190 is_error: false,
2191 },
2192 )
2193 .unwrap();
2194 region
2195 .add_typed_entry(
2196 "result 2".to_string(),
2197 5,
2198 EntryKind::ToolResult {
2199 tool_call_id: "tc_2".to_string(),
2200 tool_name: "write_file".to_string(),
2201 is_error: false,
2202 },
2203 )
2204 .unwrap();
2205
2206 assert_eq!(region.turn_group_size_at(0), 3);
2207 }
2208
2209 #[test]
2210 fn test_turn_group_size_at_assistant_at_end() {
2211 let mut region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
2212 region
2213 .add_typed_entry(
2214 "assistant with no tools".to_string(),
2215 10,
2216 EntryKind::AssistantTurn { tool_calls: vec![] },
2217 )
2218 .unwrap();
2219
2220 assert_eq!(region.turn_group_size_at(0), 1);
2221 }
2222
2223 #[test]
2224 fn test_turn_group_size_at_out_of_bounds() {
2225 let region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
2226 assert_eq!(region.turn_group_size_at(0), 0);
2227 assert_eq!(region.turn_group_size_at(99), 0);
2228 }
2229
2230 #[test]
2231 fn test_turn_group_size_at_non_assistant_entries() {
2232 let mut region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
2233 region
2234 .add_typed_entry("hello".to_string(), 5, EntryKind::Text)
2235 .unwrap();
2236 region
2237 .add_typed_entry("hi".to_string(), 5, EntryKind::UserMessage)
2238 .unwrap();
2239 region
2240 .add_typed_entry(
2241 "orphan result".to_string(),
2242 5,
2243 EntryKind::ToolResult {
2244 tool_call_id: "tc_x".to_string(),
2245 tool_name: "tool".to_string(),
2246 is_error: false,
2247 },
2248 )
2249 .unwrap();
2250
2251 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); }
2255
2256 #[test]
2259 fn test_remove_oldest_evicts_entire_turn_group() {
2260 let mut region = Region::new("conv".to_string(), RegionKind::Temporary, 50000);
2261 region
2263 .add_typed_entry(
2264 "assistant".to_string(),
2265 100,
2266 EntryKind::AssistantTurn {
2267 tool_calls: vec![
2268 SerializedToolCall {
2269 id: "tc_1".to_string(),
2270 name: "read_file".to_string(),
2271 arguments: serde_json::json!({}),
2272 thought_signature: None,
2273 },
2274 SerializedToolCall {
2275 id: "tc_2".to_string(),
2276 name: "list_dir".to_string(),
2277 arguments: serde_json::json!({}),
2278 thought_signature: None,
2279 },
2280 ],
2281 },
2282 )
2283 .unwrap();
2284 region
2285 .add_typed_entry(
2286 "result 1".to_string(),
2287 30,
2288 EntryKind::ToolResult {
2289 tool_call_id: "tc_1".to_string(),
2290 tool_name: "read_file".to_string(),
2291 is_error: false,
2292 },
2293 )
2294 .unwrap();
2295 region
2296 .add_typed_entry(
2297 "result 2".to_string(),
2298 20,
2299 EntryKind::ToolResult {
2300 tool_call_id: "tc_2".to_string(),
2301 tool_name: "list_dir".to_string(),
2302 is_error: false,
2303 },
2304 )
2305 .unwrap();
2306 region
2308 .add_typed_entry("user msg".to_string(), 10, EntryKind::UserMessage)
2309 .unwrap();
2310
2311 assert_eq!(region.entry_count(), 4);
2312 assert_eq!(region.current_tokens, 160);
2313
2314 let removed = region.remove_oldest().unwrap();
2315 assert_eq!(removed.content, "assistant");
2318 assert_eq!(removed.tokens, 100 + 30 + 20); assert_eq!(region.entry_count(), 1);
2321 assert_eq!(region.content[0].content, "user msg");
2322 assert_eq!(region.current_tokens, 10);
2323 }
2324
2325 #[test]
2328 fn test_remove_oldest_turn_group_calls_taint_remove_for_each_entry() {
2329 let mut region =
2330 Region::new("conv".to_string(), RegionKind::Temporary, 50000).with_taint_tracking();
2331
2332 region
2334 .add_typed_tainted_entry(
2335 "assistant".to_string(),
2336 10,
2337 EntryKind::AssistantTurn {
2338 tool_calls: vec![SerializedToolCall {
2339 id: "tc_1".to_string(),
2340 name: "tool".to_string(),
2341 arguments: serde_json::json!({}),
2342 thought_signature: None,
2343 }],
2344 },
2345 crate::taint::TaintLevel::Private,
2346 )
2347 .unwrap();
2348 region
2349 .add_typed_tainted_entry(
2350 "result".to_string(),
2351 5,
2352 EntryKind::ToolResult {
2353 tool_call_id: "tc_1".to_string(),
2354 tool_name: "tool".to_string(),
2355 is_error: false,
2356 },
2357 crate::taint::TaintLevel::Internal,
2358 )
2359 .unwrap();
2360 region
2361 .add_tainted_entry(
2362 "public stuff".to_string(),
2363 5,
2364 crate::taint::TaintLevel::Public,
2365 )
2366 .unwrap();
2367
2368 assert_eq!(
2369 region.taint_level(),
2370 Some(crate::taint::TaintLevel::Private)
2371 );
2372 assert_eq!(region.taint.as_ref().unwrap().entry_count(), 3);
2373
2374 let removed = region.remove_oldest().unwrap();
2376 assert_eq!(removed.content, "assistant");
2377 assert_eq!(region.entry_count(), 1);
2378 assert_eq!(region.taint.as_ref().unwrap().entry_count(), 1);
2381 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
2382 }
2383
2384 #[test]
2387 fn test_sliding_window_evicts_entire_turn_group() {
2388 let mut region = Region::new(
2389 "conv".to_string(),
2390 RegionKind::SlidingWindow {
2391 max_items: 3,
2392 eviction_strategy: EvictionStrategy::PerItem,
2393 },
2394 50000,
2395 );
2396
2397 region
2399 .add_typed_entry(
2400 "assistant".to_string(),
2401 10,
2402 EntryKind::AssistantTurn {
2403 tool_calls: vec![
2404 SerializedToolCall {
2405 id: "tc_1".to_string(),
2406 name: "t1".to_string(),
2407 arguments: serde_json::json!({}),
2408 thought_signature: None,
2409 },
2410 SerializedToolCall {
2411 id: "tc_2".to_string(),
2412 name: "t2".to_string(),
2413 arguments: serde_json::json!({}),
2414 thought_signature: None,
2415 },
2416 ],
2417 },
2418 )
2419 .unwrap();
2420 region
2421 .add_typed_entry(
2422 "r1".to_string(),
2423 5,
2424 EntryKind::ToolResult {
2425 tool_call_id: "tc_1".to_string(),
2426 tool_name: "t1".to_string(),
2427 is_error: false,
2428 },
2429 )
2430 .unwrap();
2431 region
2432 .add_typed_entry(
2433 "r2".to_string(),
2434 5,
2435 EntryKind::ToolResult {
2436 tool_call_id: "tc_2".to_string(),
2437 tool_name: "t2".to_string(),
2438 is_error: false,
2439 },
2440 )
2441 .unwrap();
2442
2443 assert_eq!(region.entry_count(), 3);
2444
2445 region
2448 .add_typed_entry("user msg".to_string(), 15, EntryKind::UserMessage)
2449 .unwrap();
2450
2451 assert_eq!(region.entry_count(), 1);
2453 assert_eq!(region.content[0].content, "user msg");
2454 assert_eq!(region.current_tokens, 15);
2455 }
2456
2457 #[test]
2460 fn test_add_entry_with_metadata_tracks_taint_as_public() {
2461 let mut region =
2462 Region::new("data".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
2463
2464 region
2465 .add_entry_with_metadata("content".to_string(), 10, serde_json::json!({"key": "val"}))
2466 .unwrap();
2467
2468 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
2469 assert_eq!(region.taint.as_ref().unwrap().entry_count(), 1);
2470 assert_eq!(
2471 region.taint.as_ref().unwrap().entry_taint(0),
2472 Some(crate::taint::TaintLevel::Public)
2473 );
2474 }
2475
2476 #[test]
2479 fn test_add_typed_entry_tracks_taint_as_public() {
2480 let mut region =
2481 Region::new("conv".to_string(), RegionKind::Temporary, 1000).with_taint_tracking();
2482
2483 region
2484 .add_typed_entry(
2485 "assistant response".to_string(),
2486 10,
2487 EntryKind::AssistantTurn { tool_calls: vec![] },
2488 )
2489 .unwrap();
2490
2491 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
2492 assert_eq!(region.taint.as_ref().unwrap().entry_count(), 1);
2493 assert_eq!(
2494 region.taint.as_ref().unwrap().entry_taint(0),
2495 Some(crate::taint::TaintLevel::Public)
2496 );
2497 }
2498
2499 #[test]
2502 fn test_per_item_strategy_evicts_one_at_a_time() {
2503 let mut region = Region::new(
2504 "conv".to_string(),
2505 RegionKind::SlidingWindow {
2506 max_items: 3,
2507 eviction_strategy: EvictionStrategy::PerItem,
2508 },
2509 50000,
2510 );
2511 for i in 0..5 {
2512 region.add_entry(format!("msg{}", i), 10).unwrap();
2513 }
2514 assert_eq!(region.entry_count(), 3);
2515 assert_eq!(region.content[0].content, "msg2");
2516 assert_eq!(region.content[1].content, "msg3");
2517 assert_eq!(region.content[2].content, "msg4");
2518 }
2519
2520 #[test]
2521 fn test_bulk_eviction_triggers_on_overflow() {
2522 let mut region = Region::new(
2523 "conv".to_string(),
2524 RegionKind::SlidingWindow {
2525 max_items: 5,
2526 eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
2527 },
2528 50000,
2529 );
2530 for i in 0..8 {
2533 region.add_entry(format!("msg{}", i), 10).unwrap();
2534 }
2535 assert_eq!(region.entry_count(), 8);
2536
2537 region.add_entry("msg8".to_string(), 10).unwrap();
2539 assert_eq!(region.entry_count(), 5);
2540 assert_eq!(region.content[0].content, "msg4");
2541 }
2542
2543 #[test]
2544 fn test_bulk_eviction_respects_turn_groups() {
2545 let mut region = Region::new(
2546 "conv".to_string(),
2547 RegionKind::SlidingWindow {
2548 max_items: 3,
2549 eviction_strategy: EvictionStrategy::Bulk { overflow: 2 },
2550 },
2551 50000,
2552 );
2553 region
2555 .add_typed_entry(
2556 "assistant".to_string(),
2557 10,
2558 EntryKind::AssistantTurn {
2559 tool_calls: vec![SerializedToolCall {
2560 id: "tc1".to_string(),
2561 name: "tool".to_string(),
2562 arguments: serde_json::json!({}),
2563 thought_signature: None,
2564 }],
2565 },
2566 )
2567 .unwrap();
2568 region
2569 .add_typed_entry(
2570 "result".to_string(),
2571 5,
2572 EntryKind::ToolResult {
2573 tool_call_id: "tc1".to_string(),
2574 tool_name: "tool".to_string(),
2575 is_error: false,
2576 },
2577 )
2578 .unwrap();
2579 region.add_entry("msg2".to_string(), 10).unwrap();
2581 region.add_entry("msg3".to_string(), 10).unwrap();
2582 region.add_entry("msg4".to_string(), 10).unwrap();
2583 assert_eq!(region.entry_count(), 5);
2585
2586 region.add_entry("msg5".to_string(), 10).unwrap();
2588 assert_eq!(region.entry_count(), 3);
2591 assert_eq!(region.content[0].content, "msg3");
2592 }
2593
2594 #[test]
2595 fn test_bulk_eviction_under_overflow_no_eviction() {
2596 let mut region = Region::new(
2597 "conv".to_string(),
2598 RegionKind::SlidingWindow {
2599 max_items: 5,
2600 eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
2601 },
2602 50000,
2603 );
2604 for i in 0..7 {
2606 region.add_entry(format!("msg{}", i), 10).unwrap();
2607 }
2608 assert_eq!(region.entry_count(), 7);
2610 }
2611
2612 #[test]
2613 fn test_compact_sets_needs_message_compaction_flag() {
2614 let mut region = Region::new(
2615 "conv".to_string(),
2616 RegionKind::SlidingWindow {
2617 max_items: 5,
2618 eviction_strategy: EvictionStrategy::Compact { compact_count: 3 },
2619 },
2620 50000,
2621 );
2622 assert!(!region.needs_message_compaction);
2623
2624 for i in 0..9 {
2626 region.add_entry(format!("msg{}", i), 10).unwrap();
2627 }
2628 assert!(region.needs_message_compaction);
2629 assert_eq!(region.entry_count(), 9);
2631 }
2632
2633 #[test]
2634 fn test_compact_fallback_to_bulk_eviction() {
2635 let mut region = Region::new(
2636 "conv".to_string(),
2637 RegionKind::SlidingWindow {
2638 max_items: 5,
2639 eviction_strategy: EvictionStrategy::Compact { compact_count: 3 },
2640 },
2641 50000,
2642 );
2643 for i in 0..12 {
2646 region.add_entry(format!("msg{}", i), 10).unwrap();
2647 }
2648 assert_eq!(region.entry_count(), 5);
2650 assert_eq!(region.content[0].content, "msg7");
2651 assert!(!region.needs_message_compaction);
2653 }
2654
2655 #[test]
2656 fn test_eviction_strategy_default_is_per_item() {
2657 assert_eq!(EvictionStrategy::default(), EvictionStrategy::PerItem);
2658 }
2659
2660 #[test]
2661 fn test_remove_entries_by_prefix() {
2662 let mut region = Region::new("system".to_string(), RegionKind::Pinned, 50000);
2663 region
2664 .add_entry("[Stage instructions: Be terse.]".to_string(), 10)
2665 .unwrap();
2666 region
2667 .add_entry("Core identity block".to_string(), 20)
2668 .unwrap();
2669 region
2670 .add_entry("[Stage instructions: Be verbose.]".to_string(), 15)
2671 .unwrap();
2672
2673 assert_eq!(region.entry_count(), 3);
2674 region.remove_entries_by_prefix("[Stage instructions:");
2675 assert_eq!(region.entry_count(), 1);
2676 assert_eq!(region.content[0].content, "Core identity block");
2677 assert_eq!(region.current_tokens, 20);
2678 }
2679
2680 #[test]
2681 fn test_remove_entries_by_prefix_with_taint_tracking() {
2682 let mut region =
2683 Region::new("system".to_string(), RegionKind::Pinned, 50000).with_taint_tracking();
2684 region
2685 .add_tainted_entry(
2686 "[Stage instructions: Be terse.]".to_string(),
2687 10,
2688 crate::taint::TaintLevel::Private,
2689 )
2690 .unwrap();
2691 region
2692 .add_tainted_entry(
2693 "Core identity block".to_string(),
2694 20,
2695 crate::taint::TaintLevel::Public,
2696 )
2697 .unwrap();
2698 region
2699 .add_tainted_entry(
2700 "[Stage instructions: Be verbose.]".to_string(),
2701 15,
2702 crate::taint::TaintLevel::Internal,
2703 )
2704 .unwrap();
2705
2706 assert_eq!(region.entry_count(), 3);
2707 assert_eq!(
2708 region.taint_level(),
2709 Some(crate::taint::TaintLevel::Private)
2710 );
2711
2712 region.remove_entries_by_prefix("[Stage instructions:");
2713 assert_eq!(region.entry_count(), 1);
2714 assert_eq!(region.content[0].content, "Core identity block");
2715 assert_eq!(region.current_tokens, 20);
2716 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
2718 assert_eq!(region.taint.as_ref().unwrap().entry_count(), 1);
2719 }
2720
2721 #[test]
2722 fn test_compact_below_threshold_no_flag() {
2723 let mut region = Region::new(
2725 "conv".to_string(),
2726 RegionKind::SlidingWindow {
2727 max_items: 5,
2728 eviction_strategy: EvictionStrategy::Compact { compact_count: 3 },
2729 },
2730 50000,
2731 );
2732 for i in 0..8 {
2733 region.add_entry(format!("msg{}", i), 10).unwrap();
2734 }
2735 assert!(!region.needs_message_compaction);
2737 assert_eq!(region.entry_count(), 8);
2738 }
2739
2740 #[test]
2741 fn test_bulk_eviction_with_taint_tracking() {
2742 let mut region = Region::new(
2743 "conv".to_string(),
2744 RegionKind::SlidingWindow {
2745 max_items: 3,
2746 eviction_strategy: EvictionStrategy::Bulk { overflow: 2 },
2747 },
2748 50000,
2749 )
2750 .with_taint_tracking();
2751
2752 region
2754 .add_tainted_entry("private".to_string(), 10, crate::taint::TaintLevel::Private)
2755 .unwrap();
2756 for i in 1..5 {
2757 region
2758 .add_tainted_entry(format!("pub{}", i), 10, crate::taint::TaintLevel::Public)
2759 .unwrap();
2760 }
2761 assert_eq!(region.entry_count(), 5);
2762
2763 region
2765 .add_tainted_entry("pub5".to_string(), 10, crate::taint::TaintLevel::Public)
2766 .unwrap();
2767 assert_eq!(region.entry_count(), 3);
2768 assert_eq!(region.taint_level(), Some(crate::taint::TaintLevel::Public));
2770 }
2771
2772 #[test]
2773 fn test_eviction_strategy_serde_roundtrip() {
2774 let bulk = EvictionStrategy::Bulk { overflow: 5 };
2775 let json = serde_json::to_string(&bulk).unwrap();
2776 let parsed: EvictionStrategy = serde_json::from_str(&json).unwrap();
2777 assert_eq!(parsed, bulk);
2778
2779 let compact = EvictionStrategy::Compact { compact_count: 10 };
2780 let json = serde_json::to_string(&compact).unwrap();
2781 let parsed: EvictionStrategy = serde_json::from_str(&json).unwrap();
2782 assert_eq!(parsed, compact);
2783
2784 let per_item = EvictionStrategy::PerItem;
2785 let json = serde_json::to_string(&per_item).unwrap();
2786 let parsed: EvictionStrategy = serde_json::from_str(&json).unwrap();
2787 assert_eq!(parsed, per_item);
2788 }
2789
2790 #[test]
2791 fn test_sliding_window_kind_equality_with_eviction_strategy() {
2792 assert_eq!(
2793 RegionKind::SlidingWindow {
2794 max_items: 10,
2795 eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
2796 },
2797 RegionKind::SlidingWindow {
2798 max_items: 10,
2799 eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
2800 }
2801 );
2802 assert_ne!(
2803 RegionKind::SlidingWindow {
2804 max_items: 10,
2805 eviction_strategy: EvictionStrategy::PerItem,
2806 },
2807 RegionKind::SlidingWindow {
2808 max_items: 10,
2809 eviction_strategy: EvictionStrategy::Bulk { overflow: 3 },
2810 }
2811 );
2812 }
2813
2814 #[test]
2815 fn test_needs_message_compaction_default_false() {
2816 let region = Region::new("conv".to_string(), RegionKind::Temporary, 1000);
2817 assert!(!region.needs_message_compaction);
2818 }
2819
2820 #[test]
2823 fn test_add_typed_entry_validates_schema() {
2824 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000)
2825 .with_schema(RegionSchema::new(ContentFormat::Json));
2826 let result = region.add_typed_entry("not json".to_string(), 5, EntryKind::Text);
2827 assert!(result.is_err());
2828 assert_eq!(region.entry_count(), 0);
2829 }
2830
2831 #[test]
2832 fn test_add_typed_entry_checks_budget() {
2833 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 10);
2834 let result = region.add_typed_entry("too big".to_string(), 20, EntryKind::UserMessage);
2835 assert!(result.is_err());
2836 assert_eq!(region.entry_count(), 0);
2837 }
2838
2839 #[test]
2840 fn test_add_tainted_entry_without_taint_tracking() {
2841 let mut region = Region::new("data".to_string(), RegionKind::Temporary, 1000);
2843 region
2844 .add_tainted_entry("data".to_string(), 10, crate::taint::TaintLevel::Private)
2845 .unwrap();
2846 assert_eq!(region.entry_count(), 1);
2847 assert_eq!(region.taint_level(), None);
2848 }
2849
2850 #[test]
2851 fn test_remove_entries_by_prefix_no_match() {
2852 let mut region = Region::new("system".to_string(), RegionKind::Pinned, 50000);
2853 region.add_entry("Keep this".to_string(), 10).unwrap();
2854 region.add_entry("And this".to_string(), 20).unwrap();
2855 region.remove_entries_by_prefix("[Stage instructions:");
2856 assert_eq!(region.entry_count(), 2);
2857 assert_eq!(region.current_tokens, 30);
2858 }
2859
2860 #[test]
2863 fn test_hashmap_region_upsert_and_get() {
2864 let mut region = Region::new(
2865 "files".to_string(),
2866 RegionKind::HashMap { max_entries: None },
2867 10000,
2868 );
2869 region
2870 .upsert_by_key("src/main.rs", "fn main() {}".to_string(), 10)
2871 .unwrap();
2872 region
2873 .upsert_by_key("src/lib.rs", "pub mod foo;".to_string(), 8)
2874 .unwrap();
2875
2876 assert_eq!(region.entry_count(), 2);
2877 assert_eq!(region.current_tokens, 18);
2878
2879 let entry = region.get_by_key("src/main.rs").unwrap();
2880 assert_eq!(entry.content, "fn main() {}");
2881 assert_eq!(entry.key.as_deref(), Some("src/main.rs"));
2882 }
2883
2884 #[test]
2885 fn test_hashmap_region_upsert_replaces_existing() {
2886 let mut region = Region::new(
2887 "files".to_string(),
2888 RegionKind::HashMap { max_entries: None },
2889 10000,
2890 );
2891 region
2892 .upsert_by_key("file.rs", "version 1".to_string(), 10)
2893 .unwrap();
2894 assert_eq!(region.current_tokens, 10);
2895
2896 region
2897 .upsert_by_key("file.rs", "version 2".to_string(), 15)
2898 .unwrap();
2899 assert_eq!(region.entry_count(), 1);
2900 assert_eq!(region.current_tokens, 15);
2901 assert_eq!(region.get_by_key("file.rs").unwrap().content, "version 2");
2902 }
2903
2904 #[test]
2905 fn test_hashmap_region_remove_by_key() {
2906 let mut region = Region::new(
2907 "files".to_string(),
2908 RegionKind::HashMap { max_entries: None },
2909 10000,
2910 );
2911 region.upsert_by_key("a.rs", "aaa".to_string(), 10).unwrap();
2912 region.upsert_by_key("b.rs", "bbb".to_string(), 20).unwrap();
2913
2914 assert!(region.remove_by_key("a.rs"));
2915 assert_eq!(region.entry_count(), 1);
2916 assert_eq!(region.current_tokens, 20);
2917 assert!(region.get_by_key("a.rs").is_none());
2918 assert!(!region.remove_by_key("nonexistent"));
2919 }
2920
2921 #[test]
2922 fn test_hashmap_region_keys() {
2923 let mut region = Region::new(
2924 "files".to_string(),
2925 RegionKind::HashMap { max_entries: None },
2926 10000,
2927 );
2928 region.upsert_by_key("x.rs", "x".to_string(), 5).unwrap();
2929 region.upsert_by_key("y.rs", "y".to_string(), 5).unwrap();
2930
2931 let keys = region.keys();
2932 assert_eq!(keys.len(), 2);
2933 assert!(keys.contains(&"x.rs"));
2934 assert!(keys.contains(&"y.rs"));
2935 }
2936
2937 #[test]
2938 fn test_hashmap_region_lru_eviction_on_max_tokens() {
2939 let mut region = Region::new(
2940 "files".to_string(),
2941 RegionKind::HashMap { max_entries: None },
2942 30, );
2944 region.upsert_by_key("a.rs", "aaa".to_string(), 10).unwrap();
2945 region.content[0].timestamp -= 100;
2947 region.upsert_by_key("b.rs", "bbb".to_string(), 10).unwrap();
2948 region.upsert_by_key("c.rs", "ccc".to_string(), 10).unwrap();
2949 assert_eq!(region.entry_count(), 3);
2950 assert_eq!(region.current_tokens, 30);
2951
2952 region.upsert_by_key("d.rs", "ddd".to_string(), 10).unwrap();
2954 assert_eq!(region.entry_count(), 3);
2955 assert!(region.get_by_key("a.rs").is_none());
2956 assert!(region.get_by_key("d.rs").is_some());
2957 }
2958
2959 #[test]
2960 fn test_hashmap_region_max_entries_eviction() {
2961 let mut region = Region::new(
2962 "files".to_string(),
2963 RegionKind::HashMap {
2964 max_entries: Some(2),
2965 },
2966 10000,
2967 );
2968 region.upsert_by_key("a.rs", "aaa".to_string(), 10).unwrap();
2969 region.content[0].timestamp -= 100; region.upsert_by_key("b.rs", "bbb".to_string(), 10).unwrap();
2971 assert_eq!(region.entry_count(), 2);
2972
2973 region.upsert_by_key("c.rs", "ccc".to_string(), 10).unwrap();
2975 assert_eq!(region.entry_count(), 2);
2976 assert!(region.get_by_key("a.rs").is_none());
2977 assert!(region.get_by_key("c.rs").is_some());
2978 }
2979
2980 #[test]
2981 fn test_hashmap_region_upsert_too_large_for_budget() {
2982 let mut region = Region::new(
2983 "files".to_string(),
2984 RegionKind::HashMap { max_entries: None },
2985 5, );
2987 let result = region.upsert_by_key("big.rs", "huge content".to_string(), 100);
2988 assert!(result.is_err());
2989 }
2990
2991 #[test]
2992 fn test_hashmap_region_kind_equality() {
2993 assert_eq!(
2994 RegionKind::HashMap {
2995 max_entries: Some(10)
2996 },
2997 RegionKind::HashMap {
2998 max_entries: Some(10)
2999 }
3000 );
3001 assert_ne!(
3002 RegionKind::HashMap {
3003 max_entries: Some(10)
3004 },
3005 RegionKind::HashMap {
3006 max_entries: Some(20)
3007 }
3008 );
3009 assert_ne!(
3010 RegionKind::HashMap { max_entries: None },
3011 RegionKind::Pinned
3012 );
3013 }
3014
3015 #[test]
3016 fn test_hashmap_cache_hint() {
3017 let kind = RegionKind::HashMap { max_entries: None };
3018 assert_eq!(kind.cache_hint(), crate::cache::CacheHint::UntilChanged);
3019 }
3020
3021 #[test]
3022 fn test_region_entry_key_default_none() {
3023 let mut region = Region::new("test".to_string(), RegionKind::Temporary, 1000);
3024 region.add_entry("content".to_string(), 10).unwrap();
3025 assert!(region.content[0].key.is_none());
3026 }
3027
3028 #[test]
3029 fn test_region_entry_key_serde_skip_when_none() {
3030 let entry = RegionEntry {
3031 content: "test".to_string(),
3032 tokens: 5,
3033 timestamp: 0,
3034 metadata: None,
3035 kind: EntryKind::default(),
3036 key: None,
3037 };
3038 let json = serde_json::to_string(&entry).unwrap();
3039 assert!(!json.contains("key"));
3040 }
3041
3042 #[test]
3043 fn test_region_entry_key_serde_roundtrip() {
3044 let entry = RegionEntry {
3045 content: "test".to_string(),
3046 tokens: 5,
3047 timestamp: 0,
3048 metadata: None,
3049 kind: EntryKind::default(),
3050 key: Some("mykey".to_string()),
3051 };
3052 let json = serde_json::to_string(&entry).unwrap();
3053 assert!(json.contains("mykey"));
3054 let back: RegionEntry = serde_json::from_str(&json).unwrap();
3055 assert_eq!(back.key.as_deref(), Some("mykey"));
3056 }
3057
3058 #[test]
3061 fn test_hashmap_region_creation_and_basic_properties() {
3062 let region = Region::new(
3063 "lookup".to_string(),
3064 RegionKind::HashMap {
3065 max_entries: Some(5),
3066 },
3067 2000,
3068 );
3069 assert_eq!(region.name, "lookup");
3070 assert_eq!(
3071 region.kind,
3072 RegionKind::HashMap {
3073 max_entries: Some(5)
3074 }
3075 );
3076 assert_eq!(region.max_tokens, 2000);
3077 assert_eq!(region.current_tokens, 0);
3078 assert_eq!(region.entry_count(), 0);
3079 assert!(region.content.is_empty());
3080 }
3081
3082 #[test]
3083 fn test_hashmap_upsert_insert_new_entry() {
3084 let mut region = Region::new(
3085 "store".to_string(),
3086 RegionKind::HashMap {
3087 max_entries: Some(5),
3088 },
3089 5000,
3090 );
3091 region
3092 .upsert_by_key("config.toml", "[package]\nname = \"foo\"".to_string(), 12)
3093 .unwrap();
3094
3095 assert_eq!(region.entry_count(), 1);
3096 assert_eq!(region.current_tokens, 12);
3097
3098 let entry = region.get_by_key("config.toml").unwrap();
3099 assert_eq!(entry.content, "[package]\nname = \"foo\"");
3100 assert_eq!(entry.tokens, 12);
3101 assert_eq!(entry.key.as_deref(), Some("config.toml"));
3102 }
3103
3104 #[test]
3105 fn test_hashmap_upsert_update_existing_entry() {
3106 let mut region = Region::new(
3107 "store".to_string(),
3108 RegionKind::HashMap { max_entries: None },
3109 5000,
3110 );
3111 region
3112 .upsert_by_key("readme.md", "# Old".to_string(), 20)
3113 .unwrap();
3114 assert_eq!(region.current_tokens, 20);
3115
3116 region
3117 .upsert_by_key("readme.md", "# New and improved".to_string(), 35)
3118 .unwrap();
3119 assert_eq!(region.entry_count(), 1);
3120 assert_eq!(region.current_tokens, 35);
3121
3122 let entry = region.get_by_key("readme.md").unwrap();
3123 assert_eq!(entry.content, "# New and improved");
3124 assert_eq!(entry.tokens, 35);
3125 }
3126
3127 #[test]
3128 fn test_hashmap_upsert_lru_eviction_on_max_tokens() {
3129 let mut region = Region::new(
3130 "files".to_string(),
3131 RegionKind::HashMap { max_entries: None },
3132 100, );
3134
3135 region
3137 .upsert_by_key("first.rs", "first content".to_string(), 40)
3138 .unwrap();
3139 region.content[0].timestamp -= 200; region
3142 .upsert_by_key("second.rs", "second content".to_string(), 40)
3143 .unwrap();
3144 region.content[1].timestamp -= 100; region
3147 .upsert_by_key("third.rs", "third content".to_string(), 20)
3148 .unwrap();
3149 region
3153 .upsert_by_key("fourth.rs", "fourth content".to_string(), 30)
3154 .unwrap();
3155
3156 assert!(region.get_by_key("first.rs").is_none());
3158 assert!(region.get_by_key("fourth.rs").is_some());
3159 assert!(region.current_tokens <= 100);
3161 }
3162
3163 #[test]
3164 fn test_hashmap_upsert_max_entries_enforcement() {
3165 let mut region = Region::new(
3166 "cache".to_string(),
3167 RegionKind::HashMap {
3168 max_entries: Some(2),
3169 },
3170 50000,
3171 );
3172
3173 region
3174 .upsert_by_key("alpha", "aaa".to_string(), 10)
3175 .unwrap();
3176 region.content[0].timestamp -= 200; region.upsert_by_key("beta", "bbb".to_string(), 10).unwrap();
3179 region.content[1].timestamp -= 100;
3180
3181 region
3182 .upsert_by_key("gamma", "ccc".to_string(), 10)
3183 .unwrap();
3184
3185 assert_eq!(region.entry_count(), 2);
3187 assert!(region.get_by_key("alpha").is_none());
3188 assert!(region.get_by_key("beta").is_some());
3189 assert!(region.get_by_key("gamma").is_some());
3190 }
3191
3192 #[test]
3193 fn test_hashmap_get_by_key_found_and_not_found() {
3194 let mut region = Region::new(
3195 "data".to_string(),
3196 RegionKind::HashMap { max_entries: None },
3197 5000,
3198 );
3199 region
3200 .upsert_by_key("exists", "hello".to_string(), 5)
3201 .unwrap();
3202
3203 let found = region.get_by_key("exists");
3205 assert!(found.is_some());
3206 assert_eq!(found.unwrap().content, "hello");
3207
3208 let missing = region.get_by_key("does_not_exist");
3210 assert!(missing.is_none());
3211 }
3212
3213 #[test]
3214 fn test_hashmap_remove_by_key_exists() {
3215 let mut region = Region::new(
3216 "data".to_string(),
3217 RegionKind::HashMap { max_entries: None },
3218 5000,
3219 );
3220 region
3221 .upsert_by_key("target", "remove me".to_string(), 25)
3222 .unwrap();
3223 assert_eq!(region.current_tokens, 25);
3224
3225 let removed = region.remove_by_key("target");
3226 assert!(removed);
3227 assert_eq!(region.entry_count(), 0);
3228 assert_eq!(region.current_tokens, 0);
3229 assert!(region.get_by_key("target").is_none());
3230 }
3231
3232 #[test]
3233 fn test_hashmap_remove_by_key_does_not_exist() {
3234 let mut region = Region::new(
3235 "data".to_string(),
3236 RegionKind::HashMap { max_entries: None },
3237 5000,
3238 );
3239 let removed = region.remove_by_key("ghost");
3240 assert!(!removed);
3241 }
3242
3243 #[test]
3244 fn test_hashmap_keys_empty_populated_after_removal() {
3245 let mut region = Region::new(
3246 "data".to_string(),
3247 RegionKind::HashMap { max_entries: None },
3248 5000,
3249 );
3250
3251 assert!(region.keys().is_empty());
3253
3254 region.upsert_by_key("one", "1".to_string(), 5).unwrap();
3256 region.upsert_by_key("two", "2".to_string(), 5).unwrap();
3257 region.upsert_by_key("three", "3".to_string(), 5).unwrap();
3258
3259 let keys = region.keys();
3260 assert_eq!(keys.len(), 3);
3261 assert!(keys.contains(&"one"));
3262 assert!(keys.contains(&"two"));
3263 assert!(keys.contains(&"three"));
3264
3265 region.remove_by_key("two");
3267 let keys = region.keys();
3268 assert_eq!(keys.len(), 2);
3269 assert!(keys.contains(&"one"));
3270 assert!(!keys.contains(&"two"));
3271 assert!(keys.contains(&"three"));
3272 }
3273
3274 #[test]
3275 fn test_region_entry_serialization_with_key_field() {
3276 let entry_with_key = RegionEntry {
3278 content: "some data".to_string(),
3279 tokens: 10,
3280 timestamp: 1234567890,
3281 metadata: None,
3282 kind: EntryKind::default(),
3283 key: Some("mykey".to_string()),
3284 };
3285 let json = serde_json::to_string(&entry_with_key).unwrap();
3286 let deserialized: RegionEntry = serde_json::from_str(&json).unwrap();
3287 assert_eq!(deserialized.key.as_deref(), Some("mykey"));
3288 assert_eq!(deserialized.content, "some data");
3289 assert_eq!(deserialized.tokens, 10);
3290
3291 let entry_no_key = RegionEntry {
3293 content: "no key data".to_string(),
3294 tokens: 7,
3295 timestamp: 1234567890,
3296 metadata: None,
3297 kind: EntryKind::default(),
3298 key: None,
3299 };
3300 let json = serde_json::to_string(&entry_no_key).unwrap();
3301 assert!(!json.contains("\"key\""));
3302 let deserialized: RegionEntry = serde_json::from_str(&json).unwrap();
3303 assert!(deserialized.key.is_none());
3304 assert_eq!(deserialized.content, "no key data");
3305 }
3306
3307 #[test]
3308 fn test_hashmap_partial_eq() {
3309 let a = RegionKind::HashMap {
3310 max_entries: Some(5),
3311 };
3312 let b = RegionKind::HashMap {
3313 max_entries: Some(5),
3314 };
3315 let c = RegionKind::HashMap {
3316 max_entries: Some(10),
3317 };
3318 let d = RegionKind::HashMap { max_entries: None };
3319
3320 assert_eq!(a, b);
3321 assert_ne!(a, c);
3322 assert_ne!(a, d);
3323 assert_ne!(c, d);
3324 assert_ne!(a, RegionKind::Pinned);
3325 assert_ne!(a, RegionKind::Temporary);
3326 }
3327
3328 #[test]
3329 fn test_hashmap_cache_hint_returns_until_changed() {
3330 let kind = RegionKind::HashMap { max_entries: None };
3331 assert_eq!(kind.cache_hint(), crate::cache::CacheHint::UntilChanged);
3332
3333 let kind_with_max = RegionKind::HashMap {
3334 max_entries: Some(10),
3335 };
3336 assert_eq!(
3337 kind_with_max.cache_hint(),
3338 crate::cache::CacheHint::UntilChanged
3339 );
3340 }
3341
3342 #[test]
3345 fn test_remove_by_key_recomputes_taint_when_tracking_enabled() {
3346 let mut region = Region::new(
3349 "kv".to_string(),
3350 RegionKind::HashMap { max_entries: None },
3351 10_000,
3352 )
3353 .with_taint_tracking();
3354 region
3355 .upsert_by_key("k1", "value one".to_string(), 10)
3356 .unwrap();
3357 region
3358 .upsert_by_key("k2", "value two".to_string(), 10)
3359 .unwrap();
3360
3361 assert!(region.remove_by_key("k1"));
3362 assert!(!region.remove_by_key("missing"));
3363 assert_eq!(region.entry_count(), 1);
3364 assert_eq!(region.current_tokens, 10);
3365 }
3366
3367 #[test]
3368 fn test_evict_lru_entry_runs_taint_fixup() {
3369 let mut region = Region::new(
3373 "kv".to_string(),
3374 RegionKind::HashMap {
3375 max_entries: Some(1),
3376 },
3377 10_000,
3378 )
3379 .with_taint_tracking();
3380 region
3381 .upsert_by_key("first", "aaa".to_string(), 10)
3382 .unwrap();
3383 region
3384 .upsert_by_key("second", "bbb".to_string(), 10)
3385 .unwrap();
3386
3387 assert_eq!(region.entry_count(), 1);
3389 assert!(region.get_by_key("second").is_some());
3390 assert!(region.get_by_key("first").is_none());
3391 }
3392
3393 #[test]
3394 fn test_evict_lru_entry_on_empty_region_is_noop() {
3395 let mut region = Region::new(
3399 "kv".to_string(),
3400 RegionKind::HashMap {
3401 max_entries: Some(4),
3402 },
3403 1000,
3404 );
3405 assert_eq!(region.entry_count(), 0);
3406 region.evict_lru_entry();
3407 assert_eq!(region.entry_count(), 0);
3408 assert_eq!(region.current_tokens, 0);
3409 }
3410
3411 #[test]
3415 fn a_keyed_entry_can_be_added_to_any_region_kind_and_found_again() {
3416 for kind in [
3417 RegionKind::Temporary,
3418 RegionKind::Clearable,
3419 RegionKind::Pinned,
3420 ] {
3421 let mut region = Region::new("r".to_string(), kind.clone(), 1000);
3422 region
3423 .add_keyed_entry("doc", "body".to_string(), 10)
3424 .unwrap();
3425 assert_eq!(
3426 region.get_by_key("doc").map(|e| e.content.as_str()),
3427 Some("body"),
3428 "{kind:?}"
3429 );
3430 assert!(region.remove_by_key("doc"), "{kind:?}");
3431 assert_eq!(region.current_tokens, 0, "{kind:?}");
3432 }
3433 }
3434
3435 #[test]
3439 fn appending_under_one_key_twice_keeps_both_entries() {
3440 let mut region = Region::new("r".to_string(), RegionKind::Temporary, 1000);
3441 region
3442 .add_keyed_entry("doc", "first".to_string(), 5)
3443 .unwrap();
3444 region
3445 .add_keyed_entry("doc", "second".to_string(), 5)
3446 .unwrap();
3447 assert_eq!(region.content.len(), 2);
3448 assert_eq!(region.current_tokens, 10);
3449 }
3450
3451 #[test]
3454 fn a_refused_keyed_write_adds_nothing() {
3455 let mut region = Region::new("r".to_string(), RegionKind::Temporary, 10);
3456 assert!(
3457 region
3458 .add_keyed_entry("doc", "too big".to_string(), 99)
3459 .is_err()
3460 );
3461 assert!(region.content.is_empty());
3462 assert_eq!(region.current_tokens, 0);
3463 }
3464
3465 #[test]
3468 fn remove_at_releases_by_position_and_reports_a_miss() {
3469 let mut region = Region::new("r".to_string(), RegionKind::Temporary, 1000);
3470 for text in ["a", "b", "c"] {
3471 region.add_entry(text.to_string(), 5).unwrap();
3472 }
3473 assert!(region.remove_at(1));
3474 assert_eq!(region.current_tokens, 10);
3475 let left: Vec<_> = region.content.iter().map(|e| e.content.as_str()).collect();
3476 assert_eq!(left, vec!["a", "c"]);
3477
3478 assert!(!region.remove_at(9), "nothing at that position");
3479 assert_eq!(region.content.len(), 2, "a miss changes nothing");
3480 }
3481
3482 #[test]
3485 fn release_oldest_takes_what_it_can_and_says_how_much() {
3486 let mut region = Region::new("r".to_string(), RegionKind::Temporary, 1000);
3487 for text in ["a", "b", "c"] {
3488 region.add_entry(text.to_string(), 5).unwrap();
3489 }
3490 assert_eq!(region.release_oldest(2), 2);
3491 assert_eq!(
3492 region.content.first().map(|e| e.content.as_str()),
3493 Some("c"),
3494 "the oldest two went"
3495 );
3496 assert_eq!(region.release_oldest(10), 1, "only one was left");
3497 assert_eq!(region.release_oldest(3), 0, "and now none");
3498 assert_eq!(region.current_tokens, 0);
3499 }
3500
3501 #[test]
3505 fn a_reject_region_distinguishes_being_full_from_an_oversized_write() {
3506 let mut region = Region::new("r".to_string(), RegionKind::Temporary, 100);
3507 region.admission = Admission::Reject;
3508
3509 let err = region
3515 .add_entry("huge".to_string(), 500)
3516 .unwrap_err()
3517 .to_string();
3518 assert!(err.contains("exceeds token budget"), "{err}");
3519
3520 region.add_entry("fits".to_string(), 90).unwrap();
3521 let err = region
3522 .add_entry("more".to_string(), 50)
3523 .unwrap_err()
3524 .to_string();
3525 assert!(err.contains("Region 'r' is full"), "{err}");
3526 assert!(err.contains("90/100 tokens"), "{err}");
3527 assert!(err.contains("release an entry"), "says what to do: {err}");
3528 }
3529
3530 #[test]
3535 fn a_reject_sliding_window_refuses_rather_than_rolling_off() {
3536 let mut region = Region::new(
3537 "r".to_string(),
3538 RegionKind::SlidingWindow {
3539 max_items: 2,
3540 eviction_strategy: EvictionStrategy::PerItem,
3541 },
3542 1000,
3543 );
3544 region.admission = Admission::Reject;
3545 region.add_entry("one".to_string(), 5).unwrap();
3546 region.add_entry("two".to_string(), 5).unwrap();
3547
3548 let err = region
3549 .add_entry("three".to_string(), 5)
3550 .unwrap_err()
3551 .to_string();
3552 assert!(err.contains("is full"), "{err}");
3553 assert_eq!(region.content.len(), 2);
3554 assert_eq!(
3555 region.content.first().map(|e| e.content.as_str()),
3556 Some("one"),
3557 "the oldest survived"
3558 );
3559
3560 let mut evicting = Region::new(
3563 "r".to_string(),
3564 RegionKind::SlidingWindow {
3565 max_items: 2,
3566 eviction_strategy: EvictionStrategy::PerItem,
3567 },
3568 1000,
3569 );
3570 for text in ["one", "two", "three"] {
3571 evicting.add_entry(text.to_string(), 5).unwrap();
3572 }
3573 assert_eq!(evicting.content.len(), 2);
3574 assert_eq!(
3575 evicting.content.first().map(|e| e.content.as_str()),
3576 Some("two"),
3577 "the oldest rolled off as it always did"
3578 );
3579 }
3580}