1pub mod assembler;
27pub mod fs_provider;
28pub mod recent_workspace_provider;
29pub mod ripgrep_provider;
30pub mod skill_catalog_provider;
31pub mod static_provider;
32
33pub use assembler::{
34 ContextAssembler, ContextAssembly, ContextAssemblyPolicy, ContextBudget, ContextSourcePolicy,
35};
36pub use fs_provider::{FileSystemContextConfig, FileSystemContextProvider};
37pub use recent_workspace_provider::RecentWorkspaceFilesContextProvider;
38pub use ripgrep_provider::{RipgrepContextConfig, RipgrepContextProvider};
39pub use skill_catalog_provider::SkillCatalogContextProvider;
40pub use static_provider::StaticContextProvider;
41
42use serde::{Deserialize, Serialize};
43use std::collections::HashMap;
44
45pub const CONTEXT_PROVENANCE_METADATA: &str = "a3s.context.provenance";
46pub const CONTEXT_PRIORITY_METADATA: &str = "a3s.context.priority";
47pub const CONTEXT_TRUST_METADATA: &str = "a3s.context.trust";
48pub const CONTEXT_FRESHNESS_METADATA: &str = "a3s.context.freshness";
49pub(crate) const CONTEXT_REQUIRED_METADATA: &str = "a3s.context.required";
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
53pub enum ContextType {
54 Memory,
56 #[default]
58 Resource,
59 Skill,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
65pub enum ContextDepth {
66 Abstract,
68 #[default]
70 Overview,
71 Full,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct ContextQuery {
78 pub query: String,
80
81 #[serde(default)]
83 pub context_types: Vec<ContextType>,
84
85 #[serde(default)]
87 pub depth: ContextDepth,
88
89 #[serde(default = "default_max_results")]
91 pub max_results: usize,
92
93 #[serde(default = "default_max_tokens")]
95 pub max_tokens: usize,
96
97 #[serde(default)]
99 pub session_id: Option<String>,
100
101 #[serde(default)]
103 pub params: HashMap<String, serde_json::Value>,
104}
105
106fn default_max_results() -> usize {
107 10
108}
109
110fn default_max_tokens() -> usize {
111 4000
112}
113
114impl ContextQuery {
115 pub fn new(query: impl Into<String>) -> Self {
117 Self {
118 query: query.into(),
119 context_types: vec![ContextType::Resource],
120 depth: ContextDepth::default(),
121 max_results: default_max_results(),
122 max_tokens: default_max_tokens(),
123 session_id: None,
124 params: HashMap::new(),
125 }
126 }
127
128 pub fn with_types(mut self, types: impl IntoIterator<Item = ContextType>) -> Self {
130 self.context_types = types.into_iter().collect();
131 self
132 }
133
134 pub fn with_depth(mut self, depth: ContextDepth) -> Self {
136 self.depth = depth;
137 self
138 }
139
140 pub fn with_max_results(mut self, max: usize) -> Self {
142 self.max_results = max;
143 self
144 }
145
146 pub fn with_max_tokens(mut self, max: usize) -> Self {
148 self.max_tokens = max;
149 self
150 }
151
152 pub fn with_session_id(mut self, id: impl Into<String>) -> Self {
154 self.session_id = Some(id.into());
155 self
156 }
157
158 pub fn with_param(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
160 self.params.insert(key.into(), value);
161 self
162 }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct ContextItem {
168 pub id: String,
170
171 pub context_type: ContextType,
173
174 pub content: String,
176
177 #[serde(default)]
179 pub token_count: usize,
180
181 #[serde(default)]
183 pub relevance: f32,
184
185 #[serde(default)]
187 pub source: Option<String>,
188
189 #[serde(default)]
191 pub metadata: HashMap<String, serde_json::Value>,
192}
193
194impl ContextItem {
195 pub fn new(
197 id: impl Into<String>,
198 context_type: ContextType,
199 content: impl Into<String>,
200 ) -> Self {
201 Self {
202 id: id.into(),
203 context_type,
204 content: content.into(),
205 token_count: 0,
206 relevance: 0.0,
207 source: None,
208 metadata: HashMap::new(),
209 }
210 }
211
212 pub fn with_token_count(mut self, count: usize) -> Self {
214 self.token_count = count;
215 self
216 }
217
218 pub fn with_relevance(mut self, score: f32) -> Self {
220 self.relevance = score.clamp(0.0, 1.0);
221 self
222 }
223
224 pub fn with_source(mut self, source: impl Into<String>) -> Self {
226 self.source = Some(source.into());
227 self
228 }
229
230 pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
232 self.metadata.insert(key.into(), value);
233 self
234 }
235
236 pub fn with_provenance(mut self, provenance: impl Into<String>) -> Self {
238 self.metadata.insert(
239 CONTEXT_PROVENANCE_METADATA.to_string(),
240 serde_json::Value::String(provenance.into()),
241 );
242 self
243 }
244
245 pub fn with_priority(mut self, priority: f32) -> Self {
247 self.metadata.insert(
248 CONTEXT_PRIORITY_METADATA.to_string(),
249 serde_json::json!(priority.clamp(0.0, 1.0)),
250 );
251 self
252 }
253
254 pub fn with_trust(mut self, trust: f32) -> Self {
256 self.metadata.insert(
257 CONTEXT_TRUST_METADATA.to_string(),
258 serde_json::json!(trust.clamp(0.0, 1.0)),
259 );
260 self
261 }
262
263 pub fn with_freshness(mut self, freshness: f32) -> Self {
265 self.metadata.insert(
266 CONTEXT_FRESHNESS_METADATA.to_string(),
267 serde_json::json!(freshness.clamp(0.0, 1.0)),
268 );
269 self
270 }
271
272 pub(crate) fn with_required(mut self) -> Self {
278 self.metadata.insert(
279 CONTEXT_REQUIRED_METADATA.to_string(),
280 serde_json::Value::Bool(true),
281 );
282 self
283 }
284
285 pub fn provenance(&self) -> Option<&str> {
286 self.metadata
287 .get(CONTEXT_PROVENANCE_METADATA)
288 .and_then(serde_json::Value::as_str)
289 }
290
291 pub fn priority(&self) -> f32 {
292 metadata_score(self.metadata.get(CONTEXT_PRIORITY_METADATA))
293 }
294
295 pub fn trust(&self) -> f32 {
296 metadata_score(self.metadata.get(CONTEXT_TRUST_METADATA))
297 }
298
299 pub fn freshness(&self) -> f32 {
300 metadata_score(self.metadata.get(CONTEXT_FRESHNESS_METADATA))
301 }
302
303 pub(crate) fn is_required(&self) -> bool {
304 self.metadata
305 .get(CONTEXT_REQUIRED_METADATA)
306 .and_then(serde_json::Value::as_bool)
307 .unwrap_or(false)
308 }
309
310 pub fn to_xml(&self) -> String {
312 let source_attr = self
313 .source
314 .as_ref()
315 .map(|s| format!(" source=\"{}\"", s))
316 .unwrap_or_default();
317 let type_str = match self.context_type {
318 ContextType::Memory => "Memory",
319 ContextType::Resource => "Resource",
320 ContextType::Skill => "Skill",
321 };
322 format!(
323 "<context{} type=\"{}\">\n{}\n</context>",
324 source_attr, type_str, self.content
325 )
326 }
327}
328
329fn metadata_score(value: Option<&serde_json::Value>) -> f32 {
330 value
331 .and_then(serde_json::Value::as_f64)
332 .map(|score| (score as f32).clamp(0.0, 1.0))
333 .unwrap_or(0.0)
334}
335
336#[derive(Debug, Clone, Default, Serialize, Deserialize)]
338pub struct ContextResult {
339 pub items: Vec<ContextItem>,
341
342 pub total_tokens: usize,
344
345 pub provider: String,
347
348 pub truncated: bool,
350}
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
354pub enum ContextProviderFailureMode {
355 #[default]
358 BestEffort,
359 FailClosed,
363}
364
365impl ContextResult {
366 pub fn new(provider: impl Into<String>) -> Self {
368 Self {
369 items: Vec::new(),
370 total_tokens: 0,
371 provider: provider.into(),
372 truncated: false,
373 }
374 }
375
376 pub fn add_item(&mut self, item: ContextItem) {
378 self.total_tokens += item.token_count;
379 self.items.push(item);
380 }
381
382 pub fn is_empty(&self) -> bool {
384 self.items.is_empty()
385 }
386
387 pub fn to_xml(&self) -> String {
389 self.items
390 .iter()
391 .map(|item| item.to_xml())
392 .collect::<Vec<_>>()
393 .join("\n\n")
394 }
395}
396
397#[async_trait::async_trait]
399pub trait ContextProvider: Send + Sync {
400 fn name(&self) -> &str;
402
403 fn failure_mode(&self) -> ContextProviderFailureMode {
405 ContextProviderFailureMode::BestEffort
406 }
407
408 fn cognitive_package_binding(
414 &self,
415 ) -> Option<&crate::cognitive_context::CognitivePackageBindingV1> {
416 None
417 }
418
419 async fn query(&self, query: &ContextQuery) -> anyhow::Result<ContextResult>;
421
422 async fn on_turn_complete(
427 &self,
428 _session_id: &str,
429 _prompt: &str,
430 _response: &str,
431 ) -> anyhow::Result<()> {
432 Ok(())
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
445 fn test_context_type_default() {
446 let ct: ContextType = Default::default();
447 assert_eq!(ct, ContextType::Resource);
448 }
449
450 #[test]
451 fn test_context_type_serialization() {
452 let ct = ContextType::Memory;
453 let json = serde_json::to_string(&ct).unwrap();
454 assert_eq!(json, "\"Memory\"");
455
456 let parsed: ContextType = serde_json::from_str(&json).unwrap();
457 assert_eq!(parsed, ContextType::Memory);
458 }
459
460 #[test]
461 fn test_context_type_all_variants() {
462 let types = vec![
463 ContextType::Memory,
464 ContextType::Resource,
465 ContextType::Skill,
466 ];
467 for ct in types {
468 let json = serde_json::to_string(&ct).unwrap();
469 let parsed: ContextType = serde_json::from_str(&json).unwrap();
470 assert_eq!(parsed, ct);
471 }
472 }
473
474 #[test]
479 fn test_context_depth_default() {
480 let cd: ContextDepth = Default::default();
481 assert_eq!(cd, ContextDepth::Overview);
482 }
483
484 #[test]
485 fn test_context_depth_serialization() {
486 let cd = ContextDepth::Full;
487 let json = serde_json::to_string(&cd).unwrap();
488 assert_eq!(json, "\"Full\"");
489
490 let parsed: ContextDepth = serde_json::from_str(&json).unwrap();
491 assert_eq!(parsed, ContextDepth::Full);
492 }
493
494 #[test]
495 fn test_context_depth_all_variants() {
496 let depths = vec![
497 ContextDepth::Abstract,
498 ContextDepth::Overview,
499 ContextDepth::Full,
500 ];
501 for cd in depths {
502 let json = serde_json::to_string(&cd).unwrap();
503 let parsed: ContextDepth = serde_json::from_str(&json).unwrap();
504 assert_eq!(parsed, cd);
505 }
506 }
507
508 #[test]
513 fn test_context_query_new() {
514 let query = ContextQuery::new("test query");
515 assert_eq!(query.query, "test query");
516 assert_eq!(query.context_types, vec![ContextType::Resource]);
517 assert_eq!(query.depth, ContextDepth::Overview);
518 assert_eq!(query.max_results, 10);
519 assert_eq!(query.max_tokens, 4000);
520 assert!(query.session_id.is_none());
521 assert!(query.params.is_empty());
522 }
523
524 #[test]
525 fn test_context_query_builder() {
526 let query = ContextQuery::new("test")
527 .with_types([ContextType::Memory, ContextType::Skill])
528 .with_depth(ContextDepth::Full)
529 .with_max_results(5)
530 .with_max_tokens(2000)
531 .with_session_id("sess-123")
532 .with_param("custom", serde_json::json!("value"));
533
534 assert_eq!(query.context_types.len(), 2);
535 assert!(query.context_types.contains(&ContextType::Memory));
536 assert!(query.context_types.contains(&ContextType::Skill));
537 assert_eq!(query.depth, ContextDepth::Full);
538 assert_eq!(query.max_results, 5);
539 assert_eq!(query.max_tokens, 2000);
540 assert_eq!(query.session_id, Some("sess-123".to_string()));
541 assert_eq!(
542 query.params.get("custom"),
543 Some(&serde_json::json!("value"))
544 );
545 }
546
547 #[test]
548 fn test_context_query_serialization() {
549 let query = ContextQuery::new("search term")
550 .with_types([ContextType::Resource])
551 .with_session_id("sess-456");
552
553 let json = serde_json::to_string(&query).unwrap();
554 let parsed: ContextQuery = serde_json::from_str(&json).unwrap();
555
556 assert_eq!(parsed.query, "search term");
557 assert_eq!(parsed.session_id, Some("sess-456".to_string()));
558 }
559
560 #[test]
561 fn test_context_query_deserialization_with_defaults() {
562 let json = r#"{"query": "minimal query"}"#;
563 let query: ContextQuery = serde_json::from_str(json).unwrap();
564
565 assert_eq!(query.query, "minimal query");
566 assert!(query.context_types.is_empty()); assert_eq!(query.depth, ContextDepth::Overview);
568 assert_eq!(query.max_results, 10);
569 assert_eq!(query.max_tokens, 4000);
570 }
571
572 #[test]
577 fn test_context_item_new() {
578 let item = ContextItem::new("item-1", ContextType::Resource, "Some content");
579 assert_eq!(item.id, "item-1");
580 assert_eq!(item.context_type, ContextType::Resource);
581 assert_eq!(item.content, "Some content");
582 assert_eq!(item.token_count, 0);
583 assert_eq!(item.relevance, 0.0);
584 assert!(item.source.is_none());
585 assert!(item.metadata.is_empty());
586 }
587
588 #[test]
589 fn test_context_item_builder() {
590 let item = ContextItem::new("item-2", ContextType::Memory, "Memory content")
591 .with_token_count(150)
592 .with_relevance(0.85)
593 .with_source("viking://memory/session-123")
594 .with_provenance("memory")
595 .with_priority(0.7)
596 .with_trust(1.2)
597 .with_freshness(-1.0)
598 .with_metadata("key", serde_json::json!("value"));
599
600 assert_eq!(item.token_count, 150);
601 assert!((item.relevance - 0.85).abs() < f32::EPSILON);
602 assert_eq!(item.source, Some("viking://memory/session-123".to_string()));
603 assert_eq!(item.provenance(), Some("memory"));
604 assert!((item.priority() - 0.7).abs() < f32::EPSILON);
605 assert!((item.trust() - 1.0).abs() < f32::EPSILON);
606 assert!(item.freshness().abs() < f32::EPSILON);
607 assert_eq!(item.metadata.get("key"), Some(&serde_json::json!("value")));
608 }
609
610 #[test]
611 fn test_context_item_relevance_clamping() {
612 let item1 = ContextItem::new("id", ContextType::Resource, "").with_relevance(1.5);
613 assert!((item1.relevance - 1.0).abs() < f32::EPSILON);
614
615 let item2 = ContextItem::new("id", ContextType::Resource, "").with_relevance(-0.5);
616 assert!(item2.relevance.abs() < f32::EPSILON);
617 }
618
619 #[test]
620 fn test_context_item_to_xml_without_source() {
621 let item = ContextItem::new("id", ContextType::Resource, "Content here");
622 let xml = item.to_xml();
623 assert_eq!(xml, "<context type=\"Resource\">\nContent here\n</context>");
624 }
625
626 #[test]
627 fn test_context_item_to_xml_with_source() {
628 let item = ContextItem::new("id", ContextType::Memory, "Memory content")
629 .with_source("viking://docs/auth");
630 let xml = item.to_xml();
631 assert_eq!(
632 xml,
633 "<context source=\"viking://docs/auth\" type=\"Memory\">\nMemory content\n</context>"
634 );
635 }
636
637 #[test]
638 fn test_context_item_to_xml_all_types() {
639 let memory = ContextItem::new("m", ContextType::Memory, "m").to_xml();
640 assert!(memory.contains("type=\"Memory\""));
641
642 let resource = ContextItem::new("r", ContextType::Resource, "r").to_xml();
643 assert!(resource.contains("type=\"Resource\""));
644
645 let skill = ContextItem::new("s", ContextType::Skill, "s").to_xml();
646 assert!(skill.contains("type=\"Skill\""));
647 }
648
649 #[test]
650 fn test_context_item_serialization() {
651 let item = ContextItem::new("item-3", ContextType::Skill, "Skill instructions")
652 .with_token_count(200)
653 .with_relevance(0.9)
654 .with_source("viking://skills/code-review");
655
656 let json = serde_json::to_string(&item).unwrap();
657 let parsed: ContextItem = serde_json::from_str(&json).unwrap();
658
659 assert_eq!(parsed.id, "item-3");
660 assert_eq!(parsed.context_type, ContextType::Skill);
661 assert_eq!(parsed.content, "Skill instructions");
662 assert_eq!(parsed.token_count, 200);
663 }
664
665 #[test]
670 fn test_context_result_new() {
671 let result = ContextResult::new("test-provider");
672 assert!(result.items.is_empty());
673 assert_eq!(result.total_tokens, 0);
674 assert_eq!(result.provider, "test-provider");
675 assert!(!result.truncated);
676 }
677
678 #[test]
679 fn test_context_result_add_item() {
680 let mut result = ContextResult::new("provider");
681 let item = ContextItem::new("id", ContextType::Resource, "content").with_token_count(100);
682 result.add_item(item);
683
684 assert_eq!(result.items.len(), 1);
685 assert_eq!(result.total_tokens, 100);
686 }
687
688 #[test]
689 fn test_context_result_add_multiple_items() {
690 let mut result = ContextResult::new("provider");
691 result.add_item(ContextItem::new("1", ContextType::Resource, "a").with_token_count(50));
692 result.add_item(ContextItem::new("2", ContextType::Memory, "b").with_token_count(75));
693 result.add_item(ContextItem::new("3", ContextType::Skill, "c").with_token_count(25));
694
695 assert_eq!(result.items.len(), 3);
696 assert_eq!(result.total_tokens, 150);
697 }
698
699 #[test]
700 fn test_context_result_is_empty() {
701 let empty = ContextResult::new("provider");
702 assert!(empty.is_empty());
703
704 let mut non_empty = ContextResult::new("provider");
705 non_empty.add_item(ContextItem::new("id", ContextType::Resource, "content"));
706 assert!(!non_empty.is_empty());
707 }
708
709 #[test]
710 fn test_context_result_to_xml() {
711 let mut result = ContextResult::new("provider");
712 result.add_item(
713 ContextItem::new("1", ContextType::Resource, "First content").with_source("source://1"),
714 );
715 result.add_item(ContextItem::new("2", ContextType::Memory, "Second content"));
716
717 let xml = result.to_xml();
718 assert!(xml.contains("<context source=\"source://1\" type=\"Resource\">"));
719 assert!(xml.contains("First content"));
720 assert!(xml.contains("<context type=\"Memory\">"));
721 assert!(xml.contains("Second content"));
722 }
723
724 #[test]
725 fn test_context_result_to_xml_empty() {
726 let result = ContextResult::new("provider");
727 let xml = result.to_xml();
728 assert!(xml.is_empty());
729 }
730
731 #[test]
732 fn test_context_result_serialization() {
733 let mut result = ContextResult::new("test-provider");
734 result.truncated = true;
735 result.add_item(ContextItem::new("id", ContextType::Resource, "content"));
736
737 let json = serde_json::to_string(&result).unwrap();
738 let parsed: ContextResult = serde_json::from_str(&json).unwrap();
739
740 assert_eq!(parsed.provider, "test-provider");
741 assert!(parsed.truncated);
742 assert_eq!(parsed.items.len(), 1);
743 }
744
745 #[test]
746 fn test_context_result_default() {
747 let result: ContextResult = Default::default();
748 assert!(result.items.is_empty());
749 assert_eq!(result.total_tokens, 0);
750 assert!(result.provider.is_empty());
751 assert!(!result.truncated);
752 }
753
754 struct MockContextProvider {
759 name: String,
760 items: Vec<ContextItem>,
761 }
762
763 impl MockContextProvider {
764 fn new(name: &str) -> Self {
765 Self {
766 name: name.to_string(),
767 items: Vec::new(),
768 }
769 }
770
771 fn with_items(mut self, items: Vec<ContextItem>) -> Self {
772 self.items = items;
773 self
774 }
775 }
776
777 #[async_trait::async_trait]
778 impl ContextProvider for MockContextProvider {
779 fn name(&self) -> &str {
780 &self.name
781 }
782
783 async fn query(&self, _query: &ContextQuery) -> anyhow::Result<ContextResult> {
784 let mut result = ContextResult::new(&self.name);
785 for item in &self.items {
786 result.add_item(item.clone());
787 }
788 Ok(result)
789 }
790 }
791
792 #[tokio::test]
793 async fn test_mock_context_provider() {
794 let provider = MockContextProvider::new("mock").with_items(vec![ContextItem::new(
795 "1",
796 ContextType::Resource,
797 "content",
798 )]);
799
800 assert_eq!(provider.name(), "mock");
801
802 let query = ContextQuery::new("test");
803 let result = provider.query(&query).await.unwrap();
804
805 assert_eq!(result.provider, "mock");
806 assert_eq!(result.items.len(), 1);
807 }
808
809 #[tokio::test]
810 async fn test_context_provider_on_turn_complete_default() {
811 let provider = MockContextProvider::new("mock");
812
813 let result = provider
815 .on_turn_complete("session-1", "prompt", "response")
816 .await;
817 assert!(result.is_ok());
818 }
819
820 struct MockMemoryProvider {
821 memories: std::sync::Arc<tokio::sync::RwLock<Vec<(String, String, String)>>>,
822 }
823
824 impl MockMemoryProvider {
825 fn new() -> Self {
826 Self {
827 memories: std::sync::Arc::new(tokio::sync::RwLock::new(Vec::new())),
828 }
829 }
830 }
831
832 #[async_trait::async_trait]
833 impl ContextProvider for MockMemoryProvider {
834 fn name(&self) -> &str {
835 "memory-provider"
836 }
837
838 async fn query(&self, _query: &ContextQuery) -> anyhow::Result<ContextResult> {
839 Ok(ContextResult::new("memory-provider"))
840 }
841
842 async fn on_turn_complete(
843 &self,
844 session_id: &str,
845 prompt: &str,
846 response: &str,
847 ) -> anyhow::Result<()> {
848 let mut memories = self.memories.write().await;
849 memories.push((
850 session_id.to_string(),
851 prompt.to_string(),
852 response.to_string(),
853 ));
854 Ok(())
855 }
856 }
857
858 #[tokio::test]
859 async fn test_context_provider_on_turn_complete_custom() {
860 let provider = MockMemoryProvider::new();
861
862 provider
863 .on_turn_complete("sess-1", "What is Rust?", "Rust is a systems language.")
864 .await
865 .unwrap();
866
867 let memories = provider.memories.read().await;
868 assert_eq!(memories.len(), 1);
869 assert_eq!(memories[0].0, "sess-1");
870 assert_eq!(memories[0].1, "What is Rust?");
871 assert_eq!(memories[0].2, "Rust is a systems language.");
872 }
873
874 #[tokio::test]
879 async fn test_multiple_providers_query() {
880 let provider1 = MockContextProvider::new("provider-1").with_items(vec![ContextItem::new(
881 "p1-1",
882 ContextType::Resource,
883 "Resource from P1",
884 )]);
885
886 let provider2 = MockContextProvider::new("provider-2").with_items(vec![
887 ContextItem::new("p2-1", ContextType::Memory, "Memory from P2"),
888 ContextItem::new("p2-2", ContextType::Skill, "Skill from P2"),
889 ]);
890
891 let providers: Vec<&dyn ContextProvider> = vec![&provider1, &provider2];
892 let query = ContextQuery::new("test");
893
894 let mut all_items = Vec::new();
895 for provider in providers {
896 let result = provider.query(&query).await.unwrap();
897 all_items.extend(result.items);
898 }
899
900 assert_eq!(all_items.len(), 3);
901 assert!(all_items.iter().any(|i| i.id == "p1-1"));
902 assert!(all_items.iter().any(|i| i.id == "p2-1"));
903 assert!(all_items.iter().any(|i| i.id == "p2-2"));
904 }
905
906 #[test]
907 fn test_context_result_xml_formatting_complex() {
908 let mut result = ContextResult::new("openviking");
909 result.add_item(
910 ContextItem::new(
911 "doc-1",
912 ContextType::Resource,
913 "Authentication uses JWT tokens stored in httpOnly cookies.",
914 )
915 .with_source("viking://docs/auth")
916 .with_token_count(50),
917 );
918 result.add_item(
919 ContextItem::new(
920 "mem-1",
921 ContextType::Memory,
922 "User prefers TypeScript over JavaScript.",
923 )
924 .with_token_count(30),
925 );
926
927 let xml = result.to_xml();
928
929 assert!(xml.contains("<context source=\"viking://docs/auth\" type=\"Resource\">"));
931 assert!(xml.contains("Authentication uses JWT tokens"));
932 assert!(xml.contains("<context type=\"Memory\">"));
933 assert!(xml.contains("User prefers TypeScript"));
934
935 assert!(xml.contains("</context>\n\n<context"));
937 }
938}