1use serde::{Deserialize, Serialize};
2
3use crate::error::{Result, SqzError};
4use crate::session_store::SessionStore;
5use crate::token_counter::TokenCounter;
6use crate::types::SessionState;
7
8const DEFAULT_MAX_SNAPSHOT_BYTES: usize = 2048;
10
11const MAX_GUIDE_CHARS: usize = 2000;
13
14#[cfg(test)]
16const MAX_GUIDE_TOKENS: u32 = 500;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
23pub enum SnapshotEventType {
24 LastPrompt,
26 Error,
28 Decision,
30 PendingTask,
32 ActiveFile,
34 GitOp,
36 Rule,
38 ToolUse,
40 Environment,
42 Dependency,
44 Progress,
46 Warning,
48 Context,
50 Learning,
52 Summary,
54}
55
56impl SnapshotEventType {
57 pub fn priority(&self) -> u8 {
59 match self {
60 Self::LastPrompt => 1,
61 Self::Error => 2,
62 Self::Decision => 3,
63 Self::PendingTask => 4,
64 Self::ActiveFile => 5,
65 Self::GitOp => 6,
66 Self::Rule => 7,
67 Self::ToolUse => 8,
68 Self::Warning => 9,
69 Self::Learning => 10,
70 Self::Progress => 11,
71 Self::Context => 12,
72 Self::Environment => 13,
73 Self::Dependency => 14,
74 Self::Summary => 15,
75 }
76 }
77
78 pub fn label(&self) -> &'static str {
80 match self {
81 Self::LastPrompt => "last_request",
82 Self::Error => "errors",
83 Self::Decision => "decisions",
84 Self::PendingTask => "tasks",
85 Self::ActiveFile => "files",
86 Self::GitOp => "git",
87 Self::Rule => "rules",
88 Self::ToolUse => "tools",
89 Self::Warning => "warnings",
90 Self::Learning => "learnings",
91 Self::Progress => "progress",
92 Self::Context => "context",
93 Self::Environment => "environment",
94 Self::Dependency => "dependencies",
95 Self::Summary => "summary",
96 }
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SnapshotEvent {
103 pub content: String,
104 pub priority: u8,
105 pub event_type: SnapshotEventType,
106}
107
108impl SnapshotEvent {
109 pub fn new(event_type: SnapshotEventType, content: String) -> Self {
110 Self {
111 priority: event_type.priority(),
112 event_type,
113 content,
114 }
115 }
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct Snapshot {
123 pub events: Vec<SnapshotEvent>,
124}
125
126impl Snapshot {
127 pub fn size_bytes(&self) -> usize {
129 serde_json::to_string(self).map(|s| s.len()).unwrap_or(0)
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct SessionGuide {
140 pub text: String,
142 pub token_count: u32,
144}
145
146pub struct SessionContinuityManager<'a> {
151 store: &'a SessionStore,
152 max_snapshot_bytes: usize,
153}
154
155impl<'a> SessionContinuityManager<'a> {
156 pub fn new(store: &'a SessionStore) -> Self {
157 Self {
158 store,
159 max_snapshot_bytes: DEFAULT_MAX_SNAPSHOT_BYTES,
160 }
161 }
162
163 pub fn with_max_bytes(mut self, max_bytes: usize) -> Self {
164 self.max_snapshot_bytes = max_bytes;
165 self
166 }
167
168 pub fn build_snapshot(&self, events: Vec<SnapshotEvent>) -> Result<Snapshot> {
176 let mut sorted = events;
178 sorted.sort_by_key(|e| e.priority);
179
180 let mut included: Vec<SnapshotEvent> = Vec::new();
181
182 for event in sorted {
183 included.push(event.clone());
185 let candidate = Snapshot { events: included.clone() };
186 if candidate.size_bytes() > self.max_snapshot_bytes {
187 included.pop();
189 break;
192 }
193 }
194
195 Ok(Snapshot { events: included })
196 }
197
198 pub fn build_snapshot_from_session(&self, session: &SessionState) -> Result<Snapshot> {
201 let mut events = Vec::new();
202
203 if let Some(turn) = session.conversation.iter().rev().find(|t| {
205 matches!(t.role, crate::types::Role::User)
206 }) {
207 events.push(SnapshotEvent::new(
208 SnapshotEventType::LastPrompt,
209 truncate(&turn.content, 512),
210 ));
211 }
212
213 let mut seen_files = std::collections::HashSet::new();
215 for record in &session.tool_usage {
216 if record.tool_name.contains("file") || record.tool_name.contains("read") {
217 if seen_files.insert(record.tool_name.clone()) {
218 events.push(SnapshotEvent::new(
219 SnapshotEventType::ActiveFile,
220 record.tool_name.clone(),
221 ));
222 }
223 }
224 }
225
226 for learning in &session.learnings {
228 events.push(SnapshotEvent::new(
229 SnapshotEventType::Decision,
230 format!("{}: {}", learning.key, learning.value),
231 ));
232 }
233
234 for turn in session.conversation.iter().rev().take(5) {
236 if matches!(turn.role, crate::types::Role::Assistant) {
237 let lower = turn.content.to_lowercase();
238 if lower.contains("error") || lower.contains("failed") || lower.contains("panic") {
239 events.push(SnapshotEvent::new(
240 SnapshotEventType::Error,
241 truncate(&turn.content, 256),
242 ));
243 }
244 }
245 }
246
247 self.build_snapshot(events)
248 }
249
250 pub fn store_snapshot(&self, session_id: &str, snapshot: &Snapshot) -> Result<()> {
253 let json = serde_json::to_string(snapshot)?;
254 let compressed = crate::types::CompressedContent {
255 data: json,
256 tokens_compressed: 0,
257 tokens_original: 0,
258 stages_applied: vec!["snapshot".to_string()],
259 compression_ratio: 1.0,
260 provenance: crate::types::Provenance::default(),
261 verify: None,
262 };
263 let hash = format!("snapshot:{session_id}");
264 self.store.save_cache_entry(&hash, &compressed)?;
265 Ok(())
266 }
267
268 pub fn load_snapshot(&self, session_id: &str) -> Result<Option<Snapshot>> {
270 let hash = format!("snapshot:{session_id}");
271 match self.store.get_cache_entry(&hash)? {
272 Some(entry) => {
273 let snapshot: Snapshot = serde_json::from_str(&entry.data)
274 .map_err(|e| SqzError::Other(format!("failed to parse snapshot: {e}")))?;
275 Ok(Some(snapshot))
276 }
277 None => Ok(None),
278 }
279 }
280
281 pub fn generate_guide(&self, snapshot: &Snapshot) -> SessionGuide {
291 let category_order: &[SnapshotEventType] = &[
294 SnapshotEventType::LastPrompt,
295 SnapshotEventType::Error,
296 SnapshotEventType::Decision,
297 SnapshotEventType::PendingTask,
298 SnapshotEventType::ActiveFile,
299 SnapshotEventType::GitOp,
300 SnapshotEventType::Rule,
301 SnapshotEventType::ToolUse,
302 SnapshotEventType::Warning,
303 SnapshotEventType::Learning,
304 SnapshotEventType::Progress,
305 SnapshotEventType::Context,
306 SnapshotEventType::Environment,
307 SnapshotEventType::Dependency,
308 SnapshotEventType::Summary,
309 ];
310
311 let mut groups: std::collections::HashMap<SnapshotEventType, Vec<&str>> =
313 std::collections::HashMap::new();
314 for event in &snapshot.events {
315 groups
316 .entry(event.event_type)
317 .or_default()
318 .push(&event.content);
319 }
320
321 let wrapper_overhead = "<session_knowledge>\n</session_knowledge>\n".len();
324 let body_budget = MAX_GUIDE_CHARS.saturating_sub(wrapper_overhead);
325 let mut body = String::with_capacity(body_budget);
326
327 for &cat in category_order {
328 if let Some(entries) = groups.get(&cat) {
329 let label = cat.label();
330 let mut line = format!("{label}:");
332 for (i, entry) in entries.iter().enumerate() {
333 if i > 0 {
334 line.push(';');
335 }
336 line.push(' ');
337 line.push_str(entry);
338 }
339 line.push('\n');
340
341 if body.len() + line.len() > body_budget {
343 let remaining = body_budget.saturating_sub(body.len());
345 if remaining > label.len() + 5 {
346 let trunc = &line[..remaining.min(line.len()).saturating_sub(2)];
348 body.push_str(trunc);
349 body.push_str("…\n");
350 }
351 break;
352 }
353 body.push_str(&line);
354 }
355 }
356
357 let text = if body.is_empty() {
359 "<session_knowledge>\n</session_knowledge>\n".to_string()
360 } else {
361 format!("<session_knowledge>\n{body}</session_knowledge>\n")
362 };
363
364 let token_count = TokenCounter::count_fast(&text);
368
369 SessionGuide { text, token_count }
370 }
371}
372
373fn truncate(s: &str, max_len: usize) -> String {
376 if s.len() <= max_len {
377 s.to_string()
378 } else {
379 let mut result = s[..max_len].to_string();
380 result.push('…');
381 result
382 }
383}
384
385#[cfg(test)]
388mod tests {
389 use super::*;
390 use crate::session_store::{apply_schema, SessionStore};
391 use rusqlite::Connection;
392
393 fn in_memory_store() -> SessionStore {
394 let conn = Connection::open_in_memory().unwrap();
395 apply_schema(&conn).unwrap();
396 SessionStore::from_connection(conn)
397 }
398
399 #[test]
400 fn test_snapshot_event_priorities() {
401 assert!(SnapshotEventType::LastPrompt.priority() < SnapshotEventType::GitOp.priority());
402 assert!(SnapshotEventType::Error.priority() < SnapshotEventType::ActiveFile.priority());
403 assert!(SnapshotEventType::Decision.priority() < SnapshotEventType::PendingTask.priority());
404 }
405
406 #[test]
407 fn test_build_snapshot_empty_events() {
408 let store = in_memory_store();
409 let mgr = SessionContinuityManager::new(&store);
410 let snapshot = mgr.build_snapshot(vec![]).unwrap();
411 assert!(snapshot.events.is_empty());
412 assert!(snapshot.size_bytes() <= DEFAULT_MAX_SNAPSHOT_BYTES);
413 }
414
415 #[test]
416 fn test_build_snapshot_fits_budget() {
417 let store = in_memory_store();
418 let mgr = SessionContinuityManager::new(&store);
419
420 let events = vec![
421 SnapshotEvent::new(SnapshotEventType::LastPrompt, "Fix the auth bug".into()),
422 SnapshotEvent::new(SnapshotEventType::ActiveFile, "src/auth.rs".into()),
423 SnapshotEvent::new(SnapshotEventType::Error, "panic at line 42".into()),
424 SnapshotEvent::new(SnapshotEventType::Decision, "Use JWT tokens".into()),
425 SnapshotEvent::new(SnapshotEventType::GitOp, "commit abc123".into()),
426 ];
427
428 let snapshot = mgr.build_snapshot(events).unwrap();
429 assert!(snapshot.size_bytes() <= DEFAULT_MAX_SNAPSHOT_BYTES);
430 assert!(!snapshot.events.is_empty());
431 }
432
433 #[test]
434 fn test_build_snapshot_drops_low_priority_when_tight() {
435 let store = in_memory_store();
436 let mgr = SessionContinuityManager::new(&store).with_max_bytes(300);
438
439 let events = vec![
440 SnapshotEvent::new(SnapshotEventType::LastPrompt, "Fix the auth bug".into()),
441 SnapshotEvent::new(SnapshotEventType::ActiveFile, "src/auth.rs".into()),
442 SnapshotEvent::new(SnapshotEventType::Error, "panic at line 42".into()),
443 SnapshotEvent::new(SnapshotEventType::GitOp, "commit abc123 with a long message".into()),
444 SnapshotEvent::new(SnapshotEventType::PendingTask, "Refactor the module".into()),
445 ];
446
447 let snapshot = mgr.build_snapshot(events).unwrap();
448 assert!(snapshot.size_bytes() <= 300);
449
450 let types: Vec<_> = snapshot.events.iter().map(|e| e.event_type).collect();
452 assert!(types.contains(&SnapshotEventType::LastPrompt));
453 }
454
455 #[test]
456 fn test_build_snapshot_preserves_priority_order() {
457 let store = in_memory_store();
458 let mgr = SessionContinuityManager::new(&store);
459
460 let events = vec![
461 SnapshotEvent::new(SnapshotEventType::GitOp, "commit".into()),
462 SnapshotEvent::new(SnapshotEventType::LastPrompt, "prompt".into()),
463 SnapshotEvent::new(SnapshotEventType::Error, "err".into()),
464 ];
465
466 let snapshot = mgr.build_snapshot(events).unwrap();
467 let priorities: Vec<u8> = snapshot.events.iter().map(|e| e.priority).collect();
468 let mut sorted = priorities.clone();
470 sorted.sort();
471 assert_eq!(priorities, sorted);
472 }
473
474 #[test]
475 fn test_store_and_load_snapshot() {
476 let store = in_memory_store();
477 let mgr = SessionContinuityManager::new(&store);
478
479 let events = vec![
480 SnapshotEvent::new(SnapshotEventType::LastPrompt, "Fix bug".into()),
481 SnapshotEvent::new(SnapshotEventType::Error, "segfault".into()),
482 ];
483 let snapshot = mgr.build_snapshot(events).unwrap();
484
485 mgr.store_snapshot("sess-1", &snapshot).unwrap();
486 let loaded = mgr.load_snapshot("sess-1").unwrap().unwrap();
487
488 assert_eq!(loaded.events.len(), snapshot.events.len());
489 assert_eq!(loaded.events[0].content, snapshot.events[0].content);
490 assert_eq!(loaded.events[1].content, snapshot.events[1].content);
491 }
492
493 #[test]
494 fn test_load_nonexistent_snapshot_returns_none() {
495 let store = in_memory_store();
496 let mgr = SessionContinuityManager::new(&store);
497 let result = mgr.load_snapshot("nonexistent").unwrap();
498 assert!(result.is_none());
499 }
500
501 #[test]
502 fn test_truncate_helper() {
503 assert_eq!(truncate("hello", 10), "hello");
504 assert_eq!(truncate("hello world", 5), "hello…");
505 }
506
507 #[test]
510 fn test_generate_guide_empty_snapshot() {
511 let store = in_memory_store();
512 let mgr = SessionContinuityManager::new(&store);
513 let snapshot = Snapshot { events: vec![] };
514
515 let guide = mgr.generate_guide(&snapshot);
516 assert!(guide.text.contains("<session_knowledge>"));
517 assert!(guide.text.contains("</session_knowledge>"));
518 assert!(guide.token_count <= MAX_GUIDE_TOKENS);
519 }
520
521 #[test]
522 fn test_generate_guide_contains_session_knowledge_directive() {
523 let store = in_memory_store();
524 let mgr = SessionContinuityManager::new(&store);
525 let snapshot = Snapshot {
526 events: vec![
527 SnapshotEvent::new(SnapshotEventType::LastPrompt, "Fix auth bug".into()),
528 ],
529 };
530
531 let guide = mgr.generate_guide(&snapshot);
532 assert!(guide.text.starts_with("<session_knowledge>\n"));
533 assert!(guide.text.ends_with("</session_knowledge>\n"));
534 }
535
536 #[test]
537 fn test_generate_guide_organizes_by_category() {
538 let store = in_memory_store();
539 let mgr = SessionContinuityManager::new(&store);
540 let snapshot = Snapshot {
541 events: vec![
542 SnapshotEvent::new(SnapshotEventType::LastPrompt, "Fix auth".into()),
543 SnapshotEvent::new(SnapshotEventType::Error, "panic at line 42".into()),
544 SnapshotEvent::new(SnapshotEventType::Decision, "Use JWT".into()),
545 SnapshotEvent::new(SnapshotEventType::PendingTask, "Refactor module".into()),
546 SnapshotEvent::new(SnapshotEventType::ActiveFile, "src/auth.rs".into()),
547 SnapshotEvent::new(SnapshotEventType::GitOp, "commit abc123".into()),
548 SnapshotEvent::new(SnapshotEventType::Rule, "No unwrap in prod".into()),
549 SnapshotEvent::new(SnapshotEventType::ToolUse, "read_file".into()),
550 SnapshotEvent::new(SnapshotEventType::Warning, "Deprecated API".into()),
551 SnapshotEvent::new(SnapshotEventType::Learning, "Project uses ESM".into()),
552 SnapshotEvent::new(SnapshotEventType::Progress, "Auth module done".into()),
553 SnapshotEvent::new(SnapshotEventType::Context, "REST API project".into()),
554 SnapshotEvent::new(SnapshotEventType::Environment, "Rust 1.75".into()),
555 SnapshotEvent::new(SnapshotEventType::Dependency, "serde 1.0".into()),
556 SnapshotEvent::new(SnapshotEventType::Summary, "Working on auth".into()),
557 ],
558 };
559
560 let guide = mgr.generate_guide(&snapshot);
561
562 assert!(guide.text.contains("last_request:"));
564 assert!(guide.text.contains("errors:"));
565 assert!(guide.text.contains("decisions:"));
566 assert!(guide.text.contains("tasks:"));
567 assert!(guide.text.contains("files:"));
568 assert!(guide.text.contains("git:"));
569 assert!(guide.text.contains("rules:"));
570 assert!(guide.text.contains("tools:"));
571 assert!(guide.text.contains("warnings:"));
572 assert!(guide.text.contains("learnings:"));
573 assert!(guide.text.contains("progress:"));
574 assert!(guide.text.contains("context:"));
575 assert!(guide.text.contains("environment:"));
576 assert!(guide.text.contains("dependencies:"));
577 assert!(guide.text.contains("summary:"));
578 }
579
580 #[test]
581 fn test_generate_guide_token_count_within_budget() {
582 let store = in_memory_store();
583 let mgr = SessionContinuityManager::new(&store);
584
585 let mut events = Vec::new();
587 for i in 0..20 {
588 events.push(SnapshotEvent::new(
589 SnapshotEventType::ActiveFile,
590 format!("src/module_{i}/handler.rs"),
591 ));
592 }
593 events.push(SnapshotEvent::new(
594 SnapshotEventType::LastPrompt,
595 "Implement the session continuity feature with all 15 categories".into(),
596 ));
597 events.push(SnapshotEvent::new(
598 SnapshotEventType::Error,
599 "thread 'main' panicked at 'index out of bounds'".into(),
600 ));
601
602 let snapshot = Snapshot { events };
603 let guide = mgr.generate_guide(&snapshot);
604
605 assert!(
606 guide.token_count <= MAX_GUIDE_TOKENS,
607 "token count {} exceeds budget {}",
608 guide.token_count,
609 MAX_GUIDE_TOKENS
610 );
611 assert!(
612 guide.text.len() <= MAX_GUIDE_CHARS + 100, "guide length {} exceeds char budget",
614 guide.text.len()
615 );
616 }
617
618 #[test]
619 fn test_generate_guide_multiple_events_same_category() {
620 let store = in_memory_store();
621 let mgr = SessionContinuityManager::new(&store);
622 let snapshot = Snapshot {
623 events: vec![
624 SnapshotEvent::new(SnapshotEventType::Error, "error 1".into()),
625 SnapshotEvent::new(SnapshotEventType::Error, "error 2".into()),
626 ],
627 };
628
629 let guide = mgr.generate_guide(&snapshot);
630 assert!(guide.text.contains("errors: error 1; error 2"));
632 }
633
634 #[test]
635 fn test_generate_guide_performance_under_50ms() {
636 let store = in_memory_store();
637 let mgr = SessionContinuityManager::new(&store);
638
639 let mut events = Vec::new();
641 for i in 0..50 {
642 events.push(SnapshotEvent::new(
643 SnapshotEventType::ActiveFile,
644 format!("src/deep/nested/path/module_{i}.rs"),
645 ));
646 }
647 events.push(SnapshotEvent::new(
648 SnapshotEventType::LastPrompt,
649 "A".repeat(512),
650 ));
651 let snapshot = Snapshot { events };
652
653 let start = std::time::Instant::now();
654 let _guide = mgr.generate_guide(&snapshot);
655 let elapsed = start.elapsed();
656
657 assert!(
658 elapsed.as_millis() < 50,
659 "generate_guide took {}ms, expected <50ms",
660 elapsed.as_millis()
661 );
662 }
663
664 #[test]
665 fn test_generate_guide_category_order_matches_priority() {
666 let store = in_memory_store();
667 let mgr = SessionContinuityManager::new(&store);
668 let snapshot = Snapshot {
669 events: vec![
670 SnapshotEvent::new(SnapshotEventType::Summary, "sum".into()),
672 SnapshotEvent::new(SnapshotEventType::LastPrompt, "prompt".into()),
673 SnapshotEvent::new(SnapshotEventType::GitOp, "commit".into()),
674 SnapshotEvent::new(SnapshotEventType::Error, "err".into()),
675 ],
676 };
677
678 let guide = mgr.generate_guide(&snapshot);
679 let pos_prompt = guide.text.find("last_request:").unwrap();
681 let pos_error = guide.text.find("errors:").unwrap();
682 let pos_git = guide.text.find("git:").unwrap();
683 let pos_summary = guide.text.find("summary:").unwrap();
684 assert!(pos_prompt < pos_error);
685 assert!(pos_error < pos_git);
686 assert!(pos_git < pos_summary);
687 }
688
689 #[test]
690 fn test_snapshot_event_type_labels_are_unique() {
691 use std::collections::HashSet;
692 let all_types = [
693 SnapshotEventType::LastPrompt,
694 SnapshotEventType::Error,
695 SnapshotEventType::Decision,
696 SnapshotEventType::PendingTask,
697 SnapshotEventType::ActiveFile,
698 SnapshotEventType::GitOp,
699 SnapshotEventType::Rule,
700 SnapshotEventType::ToolUse,
701 SnapshotEventType::Warning,
702 SnapshotEventType::Learning,
703 SnapshotEventType::Progress,
704 SnapshotEventType::Context,
705 SnapshotEventType::Environment,
706 SnapshotEventType::Dependency,
707 SnapshotEventType::Summary,
708 ];
709 let labels: HashSet<&str> = all_types.iter().map(|t| t.label()).collect();
710 assert_eq!(labels.len(), 15, "expected 15 unique category labels");
711 }
712
713 #[test]
714 fn test_snapshot_event_type_has_15_categories() {
715 use std::collections::HashSet;
717 let all_types = [
718 SnapshotEventType::LastPrompt,
719 SnapshotEventType::Error,
720 SnapshotEventType::Decision,
721 SnapshotEventType::PendingTask,
722 SnapshotEventType::ActiveFile,
723 SnapshotEventType::GitOp,
724 SnapshotEventType::Rule,
725 SnapshotEventType::ToolUse,
726 SnapshotEventType::Warning,
727 SnapshotEventType::Learning,
728 SnapshotEventType::Progress,
729 SnapshotEventType::Context,
730 SnapshotEventType::Environment,
731 SnapshotEventType::Dependency,
732 SnapshotEventType::Summary,
733 ];
734 let priorities: HashSet<u8> = all_types.iter().map(|t| t.priority()).collect();
735 assert_eq!(priorities.len(), 15, "expected 15 unique priorities");
736 }
737
738 use proptest::prelude::*;
741
742 fn arb_event_type() -> impl Strategy<Value = SnapshotEventType> {
744 prop_oneof![
745 Just(SnapshotEventType::LastPrompt),
746 Just(SnapshotEventType::Error),
747 Just(SnapshotEventType::Decision),
748 Just(SnapshotEventType::PendingTask),
749 Just(SnapshotEventType::ActiveFile),
750 Just(SnapshotEventType::GitOp),
751 Just(SnapshotEventType::Rule),
752 Just(SnapshotEventType::ToolUse),
753 Just(SnapshotEventType::Warning),
754 Just(SnapshotEventType::Learning),
755 Just(SnapshotEventType::Progress),
756 Just(SnapshotEventType::Context),
757 Just(SnapshotEventType::Environment),
758 Just(SnapshotEventType::Dependency),
759 Just(SnapshotEventType::Summary),
760 ]
761 }
762
763 fn arb_snapshot_event() -> impl Strategy<Value = SnapshotEvent> {
766 (arb_event_type(), "[a-zA-Z0-9 _/\\-.]{1,300}")
767 .prop_map(|(et, content)| SnapshotEvent::new(et, content))
768 }
769
770 proptest! {
771 #[test]
780 fn prop40_snapshot_budget_compliance(
781 events in proptest::collection::vec(arb_snapshot_event(), 0..=50),
782 budget in 128usize..=4096,
783 ) {
784 let store = in_memory_store();
785 let mgr = SessionContinuityManager::new(&store).with_max_bytes(budget);
786 let snapshot = mgr.build_snapshot(events).unwrap();
787
788 prop_assert!(
790 snapshot.size_bytes() <= budget,
791 "snapshot size {} exceeds budget {}",
792 snapshot.size_bytes(),
793 budget,
794 );
795
796 let priorities: Vec<u8> = snapshot.events.iter().map(|e| e.priority).collect();
799 let mut sorted = priorities.clone();
800 sorted.sort();
801 prop_assert_eq!(
802 priorities,
803 sorted,
804 "snapshot events are not sorted by priority"
805 );
806
807 if !snapshot.events.is_empty() {
812 let max_included_priority = snapshot
813 .events
814 .iter()
815 .map(|e| e.priority)
816 .max()
817 .unwrap();
818
819 prop_assert!(
825 max_included_priority <= 15,
826 "included priority {} out of valid range",
827 max_included_priority,
828 );
829 }
830 }
831 }
832}