1use agentic_sdk::prelude::*;
16use std::path::{Path, PathBuf};
17use std::time::Instant;
18
19use crate::{CommError, CommStore};
20
21impl From<CommError> for SisterError {
26 fn from(e: CommError) -> Self {
27 match &e {
28 CommError::ChannelNotFound(id) => {
29 SisterError::not_found(format!("channel {}", id))
30 }
31 CommError::MessageNotFound(id) => {
32 SisterError::not_found(format!("message {}", id))
33 }
34 CommError::SubscriptionNotFound(id) => {
35 SisterError::not_found(format!("subscription {}", id))
36 }
37 CommError::KeyNotFound(id) => {
38 SisterError::not_found(format!("key {}", id))
39 }
40 CommError::NotFound(what) => SisterError::not_found(what.clone()),
41 CommError::InvalidChannelName(name) => SisterError::new(
42 ErrorCode::InvalidInput,
43 format!("Invalid channel name: {}", name),
44 ),
45 CommError::InvalidContent(reason) => SisterError::new(
46 ErrorCode::InvalidInput,
47 format!("Invalid content: {}", reason),
48 ),
49 CommError::InvalidSender(reason) => SisterError::new(
50 ErrorCode::InvalidInput,
51 format!("Invalid sender: {}", reason),
52 ),
53 CommError::ChannelFull(id) => SisterError::new(
54 ErrorCode::ResourceExhausted,
55 format!("Channel {} is full", id),
56 ),
57 CommError::ChannelStateViolation(id, state) => SisterError::new(
58 ErrorCode::InvalidState,
59 format!("Channel {} is {} — operation not allowed", id, state),
60 ),
61 CommError::ConsentDenied { reason } => SisterError::new(
62 ErrorCode::PermissionDenied,
63 format!("Consent denied: {}", reason),
64 ),
65 CommError::RateLimitExceeded { limit } => SisterError::new(
66 ErrorCode::ResourceExhausted,
67 format!("Rate limit exceeded: {}", limit),
68 ),
69 CommError::InvalidFile(reason) => SisterError::new(
70 ErrorCode::ChecksumMismatch,
71 format!("Invalid .acomm file: {}", reason),
72 ),
73 CommError::Io(io_err) => {
74 SisterError::new(ErrorCode::StorageError, format!("I/O error: {}", io_err))
75 }
76 CommError::LockError(reason) => {
77 SisterError::new(ErrorCode::StorageError, format!("Lock error: {}", reason))
78 }
79 CommError::Serialization(reason) => SisterError::new(
80 ErrorCode::StorageError,
81 format!("Serialization error: {}", reason),
82 ),
83 _ => SisterError::new(ErrorCode::Internal, e.to_string()),
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
94struct SessionRecord {
95 id: ContextId,
96 session_id: u32,
97 name: String,
98 created_at: chrono::DateTime<chrono::Utc>,
99 message_count_at_start: usize,
100}
101
102pub struct CommSister {
111 store: CommStore,
112 file_path: Option<PathBuf>,
113 start_time: Instant,
114
115 current_session: Option<SessionRecord>,
117 sessions: Vec<SessionRecord>,
118 next_session_id: u32,
119}
120
121impl CommSister {
122 pub fn from_store(store: CommStore, file_path: Option<PathBuf>) -> Self {
124 Self {
125 store,
126 file_path,
127 start_time: Instant::now(),
128 current_session: None,
129 sessions: vec![],
130 next_session_id: 1,
131 }
132 }
133
134 pub fn store(&self) -> &CommStore {
136 &self.store
137 }
138
139 pub fn store_mut(&mut self) -> &mut CommStore {
141 &mut self.store
142 }
143
144 pub fn current_session_id(&self) -> Option<u32> {
146 self.current_session.as_ref().map(|s| s.session_id)
147 }
148
149 fn total_items(&self) -> usize {
151 self.store.channels.len() + self.store.messages.len()
152 }
153}
154
155impl Sister for CommSister {
160 const SISTER_TYPE: SisterType = SisterType::Comm;
161 const FILE_EXTENSION: &'static str = "acomm";
162
163 fn init(config: SisterConfig) -> SisterResult<Self>
164 where
165 Self: Sized,
166 {
167 let file_path = config.data_path.clone();
168
169 let store = if let Some(ref path) = file_path {
170 if path.exists() {
171 CommStore::load(path).map_err(SisterError::from)?
172 } else if config.create_if_missing {
173 CommStore::new()
174 } else {
175 return Err(SisterError::new(
176 ErrorCode::NotFound,
177 format!("Comm store not found: {}", path.display()),
178 ));
179 }
180 } else {
181 CommStore::new()
182 };
183
184 Ok(Self::from_store(store, file_path))
185 }
186
187 fn health(&self) -> HealthStatus {
188 HealthStatus {
189 healthy: true,
190 status: Status::Ready,
191 uptime: self.start_time.elapsed(),
192 resources: ResourceUsage {
193 memory_bytes: self.total_items() * 512, disk_bytes: 0,
195 open_handles: if self.file_path.is_some() { 1 } else { 0 },
196 },
197 warnings: vec![],
198 last_error: None,
199 }
200 }
201
202 fn version(&self) -> Version {
203 Version::new(0, 1, 0) }
205
206 fn shutdown(&mut self) -> SisterResult<()> {
207 if self.current_session.is_some() {
209 let _ = SessionManagement::end_session(self);
210 }
211
212 if let Some(ref path) = self.file_path {
214 self.store.save(path).map_err(SisterError::from)?;
215 }
216
217 Ok(())
218 }
219
220 fn capabilities(&self) -> Vec<Capability> {
221 vec![
222 Capability::new("comm_channel", "Manage communication channels"),
223 Capability::new("comm_message", "Send, receive, and manage messages"),
224 Capability::new("comm_semantic", "Semantic messaging and NLP analysis"),
225 Capability::new("comm_affect", "Emotional state tracking and propagation"),
226 Capability::new("comm_hive", "Hive-mind collective intelligence"),
227 Capability::new("comm_consent", "Consent management and privacy gates"),
228 Capability::new("comm_trust", "Trust level management between agents"),
229 Capability::new("comm_keys", "Cryptographic key management"),
230 Capability::new("comm_query", "Query relationships and grounding"),
231 Capability::new("comm_session", "Session lifecycle and logging"),
232 ]
233 }
234}
235
236impl SessionManagement for CommSister {
241 fn start_session(&mut self, name: &str) -> SisterResult<ContextId> {
242 if self.current_session.is_some() {
244 self.end_session()?;
245 }
246
247 let session_id = self.next_session_id;
248 self.next_session_id += 1;
249 let context_id = ContextId::new();
250
251 let record = SessionRecord {
252 id: context_id,
253 session_id,
254 name: name.to_string(),
255 created_at: chrono::Utc::now(),
256 message_count_at_start: self.store.messages.len(),
257 };
258
259 self.current_session = Some(record.clone());
260 self.sessions.push(record);
261
262 Ok(context_id)
263 }
264
265 fn end_session(&mut self) -> SisterResult<()> {
266 if self.current_session.is_none() {
267 return Err(SisterError::new(
268 ErrorCode::InvalidState,
269 "No active session to end",
270 ));
271 }
272 self.current_session = None;
273 Ok(())
274 }
275
276 fn current_session(&self) -> Option<ContextId> {
277 self.current_session.as_ref().map(|s| s.id)
278 }
279
280 fn current_session_info(&self) -> SisterResult<ContextInfo> {
281 let session = self
282 .current_session
283 .as_ref()
284 .ok_or_else(|| SisterError::new(ErrorCode::InvalidState, "No active session"))?;
285
286 let messages_in_session =
287 self.store.messages.len().saturating_sub(session.message_count_at_start);
288
289 Ok(ContextInfo {
290 id: session.id,
291 name: session.name.clone(),
292 created_at: session.created_at,
293 updated_at: chrono::Utc::now(),
294 item_count: messages_in_session,
295 size_bytes: messages_in_session * 512,
296 metadata: Metadata::new(),
297 })
298 }
299
300 fn list_sessions(&self) -> SisterResult<Vec<ContextSummary>> {
301 Ok(self
302 .sessions
303 .iter()
304 .rev() .map(|s| ContextSummary {
306 id: s.id,
307 name: s.name.clone(),
308 created_at: s.created_at,
309 updated_at: s.created_at,
310 item_count: 0,
311 size_bytes: 0,
312 })
313 .collect())
314 }
315
316 fn export_session(&self, id: ContextId) -> SisterResult<ContextSnapshot> {
317 let session = self
318 .sessions
319 .iter()
320 .find(|s| s.id == id)
321 .ok_or_else(|| SisterError::context_not_found(id.to_string()))?;
322
323 let session_messages: Vec<&crate::Message> = self
325 .store
326 .messages
327 .values()
328 .filter(|m| m.timestamp >= session.created_at)
329 .collect();
330
331 let data = serde_json::to_vec(&session_messages)
332 .map_err(|e| SisterError::new(ErrorCode::Internal, e.to_string()))?;
333 let checksum = *blake3::hash(&data).as_bytes();
334
335 Ok(ContextSnapshot {
336 sister_type: SisterType::Comm,
337 version: Version::new(0, 1, 0),
338 context_info: ContextInfo {
339 id,
340 name: session.name.clone(),
341 created_at: session.created_at,
342 updated_at: chrono::Utc::now(),
343 item_count: session_messages.len(),
344 size_bytes: data.len(),
345 metadata: Metadata::new(),
346 },
347 data,
348 checksum,
349 snapshot_at: chrono::Utc::now(),
350 })
351 }
352
353 fn import_session(&mut self, snapshot: ContextSnapshot) -> SisterResult<ContextId> {
354 if !snapshot.verify() {
355 return Err(SisterError::new(
356 ErrorCode::ChecksumMismatch,
357 "Session snapshot checksum verification failed",
358 ));
359 }
360
361 let context_id = self.start_session(&snapshot.context_info.name)?;
363
364 let messages: Vec<crate::Message> = serde_json::from_slice(&snapshot.data)
366 .map_err(|e| SisterError::new(ErrorCode::InvalidInput, e.to_string()))?;
367
368 for msg in messages {
369 self.store.messages.insert(msg.id, msg);
370 }
371
372 Ok(context_id)
373 }
374}
375
376impl Grounding for CommSister {
381 fn ground(&self, claim: &str) -> SisterResult<GroundingResult> {
382 let matches = self.store.search_messages(claim, 10);
383
384 if matches.is_empty() {
385 let mut recent_msgs: Vec<&crate::Message> =
386 self.store.messages.values().collect();
387 recent_msgs.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
388 let suggestions: Vec<String> = recent_msgs
389 .iter()
390 .take(3)
391 .map(|m| m.content.clone())
392 .collect();
393
394 return Ok(
395 GroundingResult::ungrounded(claim, "No matching messages found")
396 .with_suggestions(suggestions),
397 );
398 }
399
400 let evidence: Vec<GroundingEvidence> = matches
401 .iter()
402 .enumerate()
403 .map(|(i, msg)| {
404 let score = 1.0 - (i as f64 * 0.1); GroundingEvidence::new(
406 "comm_message",
407 format!("msg_{}", msg.id),
408 score,
409 &msg.content,
410 )
411 .with_data("sender", msg.sender.clone())
412 .with_data("channel_id", msg.channel_id)
413 .with_data("message_type", format!("{}", msg.message_type))
414 .with_data("timestamp", msg.timestamp.to_rfc3339())
415 })
416 .collect();
417
418 let confidence = if matches.len() >= 3 { 0.8 } else { 0.4 };
419
420 if confidence > 0.5 {
421 Ok(GroundingResult::verified(claim, confidence)
422 .with_evidence(evidence)
423 .with_reason("Found matching messages via content search"))
424 } else {
425 Ok(GroundingResult::partial(claim, confidence)
426 .with_evidence(evidence)
427 .with_reason("Some evidence found but limited matches"))
428 }
429 }
430
431 fn evidence(&self, query: &str, max_results: usize) -> SisterResult<Vec<EvidenceDetail>> {
432 let matches = self.store.search_messages(query, max_results);
433
434 Ok(matches
435 .iter()
436 .enumerate()
437 .map(|(i, msg)| {
438 let score = 1.0 - (i as f64 * 0.05);
439 EvidenceDetail {
440 evidence_type: "comm_message".to_string(),
441 id: format!("msg_{}", msg.id),
442 score,
443 created_at: msg.timestamp,
444 source_sister: SisterType::Comm,
445 content: msg.content.clone(),
446 data: {
447 let mut meta = Metadata::new();
448 if let Ok(v) = serde_json::to_value(&msg.sender) {
449 meta.insert("sender".to_string(), v);
450 }
451 if let Ok(v) = serde_json::to_value(msg.channel_id) {
452 meta.insert("channel_id".to_string(), v);
453 }
454 if let Ok(v) = serde_json::to_value(format!("{}", msg.message_type)) {
455 meta.insert("message_type".to_string(), v);
456 }
457 meta
458 },
459 }
460 })
461 .collect())
462 }
463
464 fn suggest(&self, query: &str, limit: usize) -> SisterResult<Vec<GroundingSuggestion>> {
465 let query_lower = query.to_lowercase();
467 let query_words: Vec<&str> = query_lower.split_whitespace().collect();
468
469 let mut scored: Vec<(f64, &crate::Message)> = self
470 .store
471 .messages
472 .values()
473 .map(|msg| {
474 let content_lower = msg.content.to_lowercase();
475 let matched = query_words
476 .iter()
477 .filter(|w| content_lower.contains(**w))
478 .count();
479 let score = if query_words.is_empty() {
480 0.0
481 } else {
482 matched as f64 / query_words.len() as f64
483 };
484 (score, msg)
485 })
486 .filter(|(score, _)| *score > 0.0)
487 .collect();
488
489 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
490
491 Ok(scored
492 .into_iter()
493 .take(limit)
494 .map(|(score, msg)| GroundingSuggestion {
495 item_type: "comm_message".to_string(),
496 id: format!("msg_{}", msg.id),
497 relevance_score: score,
498 description: msg.content.clone(),
499 data: Metadata::new(),
500 })
501 .collect())
502 }
503}
504
505impl Queryable for CommSister {
510 fn query(&self, query: Query) -> SisterResult<QueryResult> {
511 let start = Instant::now();
512
513 let results: Vec<serde_json::Value> = match query.query_type.as_str() {
514 "list" => {
515 let limit = query.limit.unwrap_or(50);
516 let offset = query.offset.unwrap_or(0);
517 let mut msgs: Vec<&crate::Message> = self.store.messages.values().collect();
518 msgs.sort_by_key(|m| m.id);
519 msgs.iter()
520 .skip(offset)
521 .take(limit)
522 .map(|m| {
523 serde_json::json!({
524 "id": m.id,
525 "channel_id": m.channel_id,
526 "sender": m.sender,
527 "content": m.content,
528 "message_type": format!("{}", m.message_type),
529 "timestamp": m.timestamp.to_rfc3339(),
530 })
531 })
532 .collect()
533 }
534 "search" => {
535 let text = query.get_string("text").unwrap_or_default();
536 let max = query.limit.unwrap_or(20);
537 let matches = self.store.search_messages(&text, max);
538 matches
539 .iter()
540 .map(|m| {
541 serde_json::json!({
542 "id": m.id,
543 "channel_id": m.channel_id,
544 "sender": m.sender,
545 "content": m.content,
546 "message_type": format!("{}", m.message_type),
547 "timestamp": m.timestamp.to_rfc3339(),
548 })
549 })
550 .collect()
551 }
552 "recent" => {
553 let count = query.limit.unwrap_or(10);
554 let mut msgs: Vec<&crate::Message> = self.store.messages.values().collect();
555 msgs.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
556 msgs.iter()
557 .take(count)
558 .map(|m| {
559 serde_json::json!({
560 "id": m.id,
561 "channel_id": m.channel_id,
562 "sender": m.sender,
563 "content": m.content,
564 "message_type": format!("{}", m.message_type),
565 "timestamp": m.timestamp.to_rfc3339(),
566 })
567 })
568 .collect()
569 }
570 "get" => {
571 let id_str = query.get_string("id").unwrap_or_default();
572 let id: u64 = id_str.parse().unwrap_or(0);
573 if let Some(m) = self.store.messages.get(&id) {
574 vec![serde_json::json!({
575 "id": m.id,
576 "channel_id": m.channel_id,
577 "sender": m.sender,
578 "content": m.content,
579 "message_type": format!("{}", m.message_type),
580 "timestamp": m.timestamp.to_rfc3339(),
581 })]
582 } else {
583 vec![]
584 }
585 }
586 "channels" => {
587 let limit = query.limit.unwrap_or(50);
588 let mut channels: Vec<&crate::Channel> = self.store.channels.values().collect();
589 channels.sort_by_key(|c| c.id);
590 channels
591 .iter()
592 .take(limit)
593 .map(|c| {
594 serde_json::json!({
595 "id": c.id,
596 "name": c.name,
597 "channel_type": format!("{:?}", c.channel_type),
598 "state": format!("{:?}", c.state),
599 "participants": c.participants.len(),
600 "created_at": c.created_at.to_rfc3339(),
601 })
602 })
603 .collect()
604 }
605 _ => vec![],
606 };
607
608 let total = self.store.messages.len();
609 let has_more = results.len() < total;
610
611 Ok(QueryResult::new(query, results, start.elapsed()).with_pagination(total, has_more))
612 }
613
614 fn supports_query(&self, query_type: &str) -> bool {
615 matches!(
616 query_type,
617 "list" | "search" | "recent" | "get" | "channels"
618 )
619 }
620
621 fn query_types(&self) -> Vec<QueryTypeInfo> {
622 vec![
623 QueryTypeInfo::new("list", "List all messages with pagination")
624 .optional(vec!["limit", "offset"]),
625 QueryTypeInfo::new("search", "Search messages by content")
626 .required(vec!["text"])
627 .optional(vec!["limit"]),
628 QueryTypeInfo::new("recent", "Get most recent messages").optional(vec!["limit"]),
629 QueryTypeInfo::new("get", "Get a specific message by ID").required(vec!["id"]),
630 QueryTypeInfo::new("channels", "List all channels").optional(vec!["limit"]),
631 ]
632 }
633}
634
635impl FileFormatReader for CommSister {
640 fn read_file(path: &Path) -> SisterResult<Self> {
641 let store = CommStore::load(path).map_err(SisterError::from)?;
642 Ok(Self::from_store(store, Some(path.to_path_buf())))
643 }
644
645 fn can_read(path: &Path) -> SisterResult<FileInfo> {
646 let data = std::fs::read(path)
647 .map_err(|e| SisterError::new(ErrorCode::StorageError, e.to_string()))?;
648 if data.len() < crate::format::FileHeader::SIZE_V2 {
649 return Err(SisterError::new(
650 ErrorCode::StorageError,
651 "File too small for .acomm format",
652 ));
653 }
654 if &data[..4] != crate::format::MAGIC {
656 return Err(SisterError::new(
657 ErrorCode::ChecksumMismatch,
658 "Not a valid .acomm file (bad magic bytes)",
659 ));
660 }
661 let version = u16::from_le_bytes([data[4], data[5]]);
662
663 let metadata = std::fs::metadata(path)
664 .map_err(|e| SisterError::new(ErrorCode::StorageError, e.to_string()))?;
665
666 Ok(FileInfo {
667 sister_type: SisterType::Comm,
668 version: Version::new(version as u8, 0, 0),
669 created_at: chrono::Utc::now(),
670 updated_at: chrono::DateTime::from(
671 metadata
672 .modified()
673 .unwrap_or(std::time::SystemTime::UNIX_EPOCH),
674 ),
675 content_length: metadata.len(),
676 needs_migration: version < crate::format::FORMAT_VERSION,
677 format_id: "ACOM".to_string(),
678 })
679 }
680
681 fn file_version(path: &Path) -> SisterResult<Version> {
682 let data = std::fs::read(path)
683 .map_err(|e| SisterError::new(ErrorCode::StorageError, e.to_string()))?;
684 if data.len() < 6 {
685 return Err(SisterError::new(
686 ErrorCode::StorageError,
687 "File too small for .acomm format",
688 ));
689 }
690 let version = u16::from_le_bytes([data[4], data[5]]);
691 Ok(Version::new(version as u8, 0, 0))
692 }
693
694 fn migrate(_data: &[u8], _from_version: Version) -> SisterResult<Vec<u8>> {
695 Err(SisterError::new(
696 ErrorCode::NotImplemented,
697 "No migration path available (v2→v3 handled transparently on load)",
698 ))
699 }
700}
701
702impl FileFormatWriter for CommSister {
703 fn write_file(&self, path: &Path) -> SisterResult<()> {
704 self.store.save(path).map_err(SisterError::from)
705 }
706
707 fn to_bytes(&self) -> SisterResult<Vec<u8>> {
708 let serialized = bincode::serialize(&self.store)
709 .map_err(|e| SisterError::new(ErrorCode::StorageError, e.to_string()))?;
710 Ok(crate::format::write_with_header(&serialized))
711 }
712}
713
714#[cfg(test)]
719mod tests {
720 use super::*;
721
722 fn make_test_sister() -> CommSister {
723 let config = SisterConfig::stateless();
724 CommSister::init(config).unwrap()
725 }
726
727 fn add_test_messages(sister: &mut CommSister) {
728 sister
730 .store
731 .create_channel("test-channel", crate::ChannelType::Group, None)
732 .unwrap();
733 let channel_id = sister.store.channels.keys().next().copied().unwrap();
734
735 sister
737 .store
738 .send_message(channel_id, "alice", "The deployment succeeded on staging", crate::MessageType::Text)
739 .unwrap();
740 sister
741 .store
742 .send_message(channel_id, "bob", "Rust compiler caught the bug early", crate::MessageType::Text)
743 .unwrap();
744 sister
745 .store
746 .send_message(channel_id, "carol", "Review the pull request before merging", crate::MessageType::Command)
747 .unwrap();
748 }
749
750 #[test]
751 fn test_sister_trait() {
752 let sister = make_test_sister();
753 assert_eq!(sister.sister_type(), SisterType::Comm);
754 assert_eq!(sister.file_extension(), "acomm");
755 assert_eq!(sister.mcp_prefix(), "comm");
756 assert!(sister.is_healthy());
757 assert_eq!(sister.version(), Version::new(0, 1, 0));
758 assert!(!sister.capabilities().is_empty());
759 }
760
761 #[test]
762 fn test_sister_info() {
763 let sister = make_test_sister();
764 let info = SisterInfo::from_sister(&sister);
765 assert_eq!(info.sister_type, SisterType::Comm);
766 assert_eq!(info.file_extension, "acomm");
767 assert_eq!(info.mcp_prefix, "comm");
768 }
769
770 #[test]
771 fn test_session_management() {
772 let mut sister = make_test_sister();
773
774 assert!(sister.current_session().is_none());
776 assert!(sister.current_session_info().is_err());
777
778 let sid = sister.start_session("test_session").unwrap();
780 assert!(sister.current_session().is_some());
781 assert_eq!(sister.current_session().unwrap(), sid);
782
783 let info = sister.current_session_info().unwrap();
785 assert_eq!(info.name, "test_session");
786
787 let sessions = sister.list_sessions().unwrap();
789 assert_eq!(sessions.len(), 1);
790 assert_eq!(sessions[0].name, "test_session");
791
792 sister.end_session().unwrap();
794 assert!(sister.current_session().is_none());
795
796 assert!(sister.end_session().is_err());
798 }
799
800 #[test]
801 fn test_grounding_with_data() {
802 let mut sister = make_test_sister();
803 sister.start_session("grounding_test").unwrap();
804 add_test_messages(&mut sister);
805
806 let result = sister.ground("deployment succeeded").unwrap();
808 assert!(
809 result.status == GroundingStatus::Verified || result.status == GroundingStatus::Partial,
810 "Expected verified or partial, got {:?}",
811 result.status
812 );
813 assert!(!result.evidence.is_empty());
814
815 let result = sister.ground("quantum teleportation physics").unwrap();
817 assert_eq!(result.status, GroundingStatus::Ungrounded);
818 }
819
820 #[test]
821 fn test_evidence_query() {
822 let mut sister = make_test_sister();
823 sister.start_session("evidence_test").unwrap();
824 add_test_messages(&mut sister);
825
826 let evidence = sister.evidence("rust", 10).unwrap();
827 assert!(!evidence.is_empty(), "Expected evidence for 'rust' query");
828 assert_eq!(evidence[0].source_sister, SisterType::Comm);
829 }
830
831 #[test]
832 fn test_suggest_fallback() {
833 let mut sister = make_test_sister();
834 sister.start_session("suggest_test").unwrap();
835 add_test_messages(&mut sister);
836
837 let suggestions = sister.suggest("deployment staging", 5).unwrap();
838 assert!(!suggestions.is_empty());
839 assert!(suggestions[0].relevance_score > 0.0);
840 }
841
842 #[test]
843 fn test_queryable_list() {
844 let mut sister = make_test_sister();
845 sister.start_session("query_test").unwrap();
846 add_test_messages(&mut sister);
847
848 let result = sister.query(Query::list().limit(2)).unwrap();
849 assert_eq!(result.len(), 2);
850 assert!(result.has_more);
851 }
852
853 #[test]
854 fn test_queryable_recent() {
855 let mut sister = make_test_sister();
856 sister.start_session("recent_test").unwrap();
857 add_test_messages(&mut sister);
858
859 let result = sister.recent(2).unwrap();
860 assert_eq!(result.len(), 2);
861 }
862
863 #[test]
864 fn test_queryable_search() {
865 let mut sister = make_test_sister();
866 sister.start_session("search_test").unwrap();
867 add_test_messages(&mut sister);
868
869 let result = sister.search("rust").unwrap();
870 assert!(!result.is_empty(), "Expected search results for 'rust'");
871 }
872
873 #[test]
874 fn test_queryable_types() {
875 let sister = make_test_sister();
876 assert!(sister.supports_query("list"));
877 assert!(sister.supports_query("search"));
878 assert!(sister.supports_query("recent"));
879 assert!(sister.supports_query("get"));
880 assert!(sister.supports_query("channels"));
881 assert!(!sister.supports_query("nonexistent"));
882
883 let types = sister.query_types();
884 assert_eq!(types.len(), 5);
885 }
886
887 #[test]
888 fn test_error_bridge() {
889 let comm_err = CommError::ChannelNotFound(42);
890 let sister_err: SisterError = comm_err.into();
891 assert_eq!(sister_err.code, ErrorCode::NotFound);
892 assert!(sister_err.message.contains("42"));
893
894 let comm_err2 = CommError::InvalidFile("corrupt header".to_string());
895 let sister_err2: SisterError = comm_err2.into();
896 assert_eq!(sister_err2.code, ErrorCode::ChecksumMismatch);
897 }
898
899 #[test]
900 fn test_session_export_import() {
901 let mut sister = make_test_sister();
902 let sid = sister.start_session("export_test").unwrap();
903 add_test_messages(&mut sister);
904
905 let snapshot = sister.export_session(sid).unwrap();
907 assert!(snapshot.verify());
908 assert_eq!(snapshot.sister_type, SisterType::Comm);
909
910 let mut sister2 = make_test_sister();
912 let _imported_sid = sister2.import_session(snapshot).unwrap();
913 assert!(sister2.current_session().is_some());
914 assert!(sister2.store().messages.len() > 0);
916 }
917
918 #[test]
919 fn test_config_patterns() {
920 let config = SisterConfig::stateless();
922 let sister = CommSister::init(config).unwrap();
923 assert!(sister.is_healthy());
924 }
925
926 #[test]
927 fn test_shutdown() {
928 let mut sister = make_test_sister();
929 sister.start_session("shutdown_test").unwrap();
930 sister.shutdown().unwrap();
931 assert!(sister.current_session().is_none());
933 }
934}