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