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=\"{}\"", escape_xml_attribute(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,
325 type_str,
326 escape_xml_text(&self.content)
327 )
328 }
329}
330
331fn escape_xml_attribute(value: &str) -> String {
332 value.chars().fold(String::new(), |mut escaped, character| {
333 match character {
334 '&' => escaped.push_str("&"),
335 '<' => escaped.push_str("<"),
336 '>' => escaped.push_str(">"),
337 '"' => escaped.push_str("""),
338 '\'' => escaped.push_str("'"),
339 _ => escaped.push(character),
340 }
341 escaped
342 })
343}
344
345fn escape_xml_text(value: &str) -> String {
346 value.chars().fold(String::new(), |mut escaped, character| {
347 match character {
348 '&' => escaped.push_str("&"),
349 '<' => escaped.push_str("<"),
350 '>' => escaped.push_str(">"),
351 _ => escaped.push(character),
352 }
353 escaped
354 })
355}
356
357fn metadata_score(value: Option<&serde_json::Value>) -> f32 {
358 value
359 .and_then(serde_json::Value::as_f64)
360 .map(|score| (score as f32).clamp(0.0, 1.0))
361 .unwrap_or(0.0)
362}
363
364pub(crate) fn read_utf8_file_bounded(
371 path: &std::path::Path,
372 max_bytes: usize,
373) -> std::io::Result<Option<String>> {
374 let bytes = match crate::bounded_io::read_file_bounded(path, max_bytes) {
375 Ok(bytes) => bytes,
376 Err(error) if error.kind() == std::io::ErrorKind::InvalidData => return Ok(None),
377 Err(error) => return Err(error),
378 };
379
380 Ok(String::from_utf8(bytes).ok())
381}
382
383#[derive(Debug, Clone, Default, Serialize, Deserialize)]
385pub struct ContextResult {
386 pub items: Vec<ContextItem>,
388
389 pub total_tokens: usize,
391
392 pub provider: String,
394
395 pub truncated: bool,
397}
398
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
401pub enum ContextProviderFailureMode {
402 #[default]
405 BestEffort,
406 FailClosed,
410}
411
412impl ContextResult {
413 pub fn new(provider: impl Into<String>) -> Self {
415 Self {
416 items: Vec::new(),
417 total_tokens: 0,
418 provider: provider.into(),
419 truncated: false,
420 }
421 }
422
423 pub fn add_item(&mut self, item: ContextItem) {
425 self.total_tokens = self.total_tokens.saturating_add(item.token_count);
426 self.items.push(item);
427 }
428
429 pub fn is_empty(&self) -> bool {
431 self.items.is_empty()
432 }
433
434 pub fn to_xml(&self) -> String {
436 self.items
437 .iter()
438 .map(|item| item.to_xml())
439 .collect::<Vec<_>>()
440 .join("\n\n")
441 }
442}
443
444#[async_trait::async_trait]
446pub trait ContextProvider: Send + Sync {
447 fn name(&self) -> &str;
449
450 fn failure_mode(&self) -> ContextProviderFailureMode {
452 ContextProviderFailureMode::BestEffort
453 }
454
455 fn cognitive_package_binding(
461 &self,
462 ) -> Option<&crate::cognitive_context::CognitivePackageBindingV1> {
463 None
464 }
465
466 async fn query(&self, query: &ContextQuery) -> anyhow::Result<ContextResult>;
468
469 async fn on_turn_complete(
474 &self,
475 _session_id: &str,
476 _prompt: &str,
477 _response: &str,
478 ) -> anyhow::Result<()> {
479 Ok(())
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 #[test]
492 fn test_context_type_default() {
493 let ct: ContextType = Default::default();
494 assert_eq!(ct, ContextType::Resource);
495 }
496
497 #[test]
498 fn test_context_type_serialization() {
499 let ct = ContextType::Memory;
500 let json = serde_json::to_string(&ct).unwrap();
501 assert_eq!(json, "\"Memory\"");
502
503 let parsed: ContextType = serde_json::from_str(&json).unwrap();
504 assert_eq!(parsed, ContextType::Memory);
505 }
506
507 #[test]
508 fn test_context_type_all_variants() {
509 let types = vec![
510 ContextType::Memory,
511 ContextType::Resource,
512 ContextType::Skill,
513 ];
514 for ct in types {
515 let json = serde_json::to_string(&ct).unwrap();
516 let parsed: ContextType = serde_json::from_str(&json).unwrap();
517 assert_eq!(parsed, ct);
518 }
519 }
520
521 #[test]
526 fn test_context_depth_default() {
527 let cd: ContextDepth = Default::default();
528 assert_eq!(cd, ContextDepth::Overview);
529 }
530
531 #[test]
532 fn test_context_depth_serialization() {
533 let cd = ContextDepth::Full;
534 let json = serde_json::to_string(&cd).unwrap();
535 assert_eq!(json, "\"Full\"");
536
537 let parsed: ContextDepth = serde_json::from_str(&json).unwrap();
538 assert_eq!(parsed, ContextDepth::Full);
539 }
540
541 #[test]
542 fn test_context_depth_all_variants() {
543 let depths = vec![
544 ContextDepth::Abstract,
545 ContextDepth::Overview,
546 ContextDepth::Full,
547 ];
548 for cd in depths {
549 let json = serde_json::to_string(&cd).unwrap();
550 let parsed: ContextDepth = serde_json::from_str(&json).unwrap();
551 assert_eq!(parsed, cd);
552 }
553 }
554
555 #[test]
560 fn test_context_query_new() {
561 let query = ContextQuery::new("test query");
562 assert_eq!(query.query, "test query");
563 assert_eq!(query.context_types, vec![ContextType::Resource]);
564 assert_eq!(query.depth, ContextDepth::Overview);
565 assert_eq!(query.max_results, 10);
566 assert_eq!(query.max_tokens, 4000);
567 assert!(query.session_id.is_none());
568 assert!(query.params.is_empty());
569 }
570
571 #[test]
572 fn test_context_query_builder() {
573 let query = ContextQuery::new("test")
574 .with_types([ContextType::Memory, ContextType::Skill])
575 .with_depth(ContextDepth::Full)
576 .with_max_results(5)
577 .with_max_tokens(2000)
578 .with_session_id("sess-123")
579 .with_param("custom", serde_json::json!("value"));
580
581 assert_eq!(query.context_types.len(), 2);
582 assert!(query.context_types.contains(&ContextType::Memory));
583 assert!(query.context_types.contains(&ContextType::Skill));
584 assert_eq!(query.depth, ContextDepth::Full);
585 assert_eq!(query.max_results, 5);
586 assert_eq!(query.max_tokens, 2000);
587 assert_eq!(query.session_id, Some("sess-123".to_string()));
588 assert_eq!(
589 query.params.get("custom"),
590 Some(&serde_json::json!("value"))
591 );
592 }
593
594 #[test]
595 fn test_context_query_serialization() {
596 let query = ContextQuery::new("search term")
597 .with_types([ContextType::Resource])
598 .with_session_id("sess-456");
599
600 let json = serde_json::to_string(&query).unwrap();
601 let parsed: ContextQuery = serde_json::from_str(&json).unwrap();
602
603 assert_eq!(parsed.query, "search term");
604 assert_eq!(parsed.session_id, Some("sess-456".to_string()));
605 }
606
607 #[test]
608 fn test_context_query_deserialization_with_defaults() {
609 let json = r#"{"query": "minimal query"}"#;
610 let query: ContextQuery = serde_json::from_str(json).unwrap();
611
612 assert_eq!(query.query, "minimal query");
613 assert!(query.context_types.is_empty()); assert_eq!(query.depth, ContextDepth::Overview);
615 assert_eq!(query.max_results, 10);
616 assert_eq!(query.max_tokens, 4000);
617 }
618
619 #[test]
624 fn test_context_item_new() {
625 let item = ContextItem::new("item-1", ContextType::Resource, "Some content");
626 assert_eq!(item.id, "item-1");
627 assert_eq!(item.context_type, ContextType::Resource);
628 assert_eq!(item.content, "Some content");
629 assert_eq!(item.token_count, 0);
630 assert_eq!(item.relevance, 0.0);
631 assert!(item.source.is_none());
632 assert!(item.metadata.is_empty());
633 }
634
635 #[test]
636 fn test_context_item_builder() {
637 let item = ContextItem::new("item-2", ContextType::Memory, "Memory content")
638 .with_token_count(150)
639 .with_relevance(0.85)
640 .with_source("viking://memory/session-123")
641 .with_provenance("memory")
642 .with_priority(0.7)
643 .with_trust(1.2)
644 .with_freshness(-1.0)
645 .with_metadata("key", serde_json::json!("value"));
646
647 assert_eq!(item.token_count, 150);
648 assert!((item.relevance - 0.85).abs() < f32::EPSILON);
649 assert_eq!(item.source, Some("viking://memory/session-123".to_string()));
650 assert_eq!(item.provenance(), Some("memory"));
651 assert!((item.priority() - 0.7).abs() < f32::EPSILON);
652 assert!((item.trust() - 1.0).abs() < f32::EPSILON);
653 assert!(item.freshness().abs() < f32::EPSILON);
654 assert_eq!(item.metadata.get("key"), Some(&serde_json::json!("value")));
655 }
656
657 #[test]
658 fn test_context_item_relevance_clamping() {
659 let item1 = ContextItem::new("id", ContextType::Resource, "").with_relevance(1.5);
660 assert!((item1.relevance - 1.0).abs() < f32::EPSILON);
661
662 let item2 = ContextItem::new("id", ContextType::Resource, "").with_relevance(-0.5);
663 assert!(item2.relevance.abs() < f32::EPSILON);
664 }
665
666 #[test]
667 fn test_context_item_to_xml_without_source() {
668 let item = ContextItem::new("id", ContextType::Resource, "Content here");
669 let xml = item.to_xml();
670 assert_eq!(xml, "<context type=\"Resource\">\nContent here\n</context>");
671 }
672
673 #[test]
674 fn test_context_item_to_xml_with_source() {
675 let item = ContextItem::new("id", ContextType::Memory, "Memory content")
676 .with_source("viking://docs/auth");
677 let xml = item.to_xml();
678 assert_eq!(
679 xml,
680 "<context source=\"viking://docs/auth\" type=\"Memory\">\nMemory content\n</context>"
681 );
682 }
683
684 #[test]
685 fn test_context_item_to_xml_escapes_untrusted_source_and_content() {
686 let item = ContextItem::new(
687 "item",
688 ContextType::Resource,
689 "<directive>& keep \"quotes\"",
690 )
691 .with_source("provider://a\"&b<c>");
692
693 assert_eq!(
694 item.to_xml(),
695 "<context source=\"provider://a"&b<c>\" type=\"Resource\">\n<directive>& keep \"quotes\"\n</context>"
696 );
697 }
698
699 #[test]
700 fn test_context_item_to_xml_all_types() {
701 let memory = ContextItem::new("m", ContextType::Memory, "m").to_xml();
702 assert!(memory.contains("type=\"Memory\""));
703
704 let resource = ContextItem::new("r", ContextType::Resource, "r").to_xml();
705 assert!(resource.contains("type=\"Resource\""));
706
707 let skill = ContextItem::new("s", ContextType::Skill, "s").to_xml();
708 assert!(skill.contains("type=\"Skill\""));
709 }
710
711 #[test]
712 fn test_context_item_serialization() {
713 let item = ContextItem::new("item-3", ContextType::Skill, "Skill instructions")
714 .with_token_count(200)
715 .with_relevance(0.9)
716 .with_source("viking://skills/code-review");
717
718 let json = serde_json::to_string(&item).unwrap();
719 let parsed: ContextItem = serde_json::from_str(&json).unwrap();
720
721 assert_eq!(parsed.id, "item-3");
722 assert_eq!(parsed.context_type, ContextType::Skill);
723 assert_eq!(parsed.content, "Skill instructions");
724 assert_eq!(parsed.token_count, 200);
725 }
726
727 #[test]
732 fn test_context_result_new() {
733 let result = ContextResult::new("test-provider");
734 assert!(result.items.is_empty());
735 assert_eq!(result.total_tokens, 0);
736 assert_eq!(result.provider, "test-provider");
737 assert!(!result.truncated);
738 }
739
740 #[test]
741 fn test_context_result_add_item() {
742 let mut result = ContextResult::new("provider");
743 let item = ContextItem::new("id", ContextType::Resource, "content").with_token_count(100);
744 result.add_item(item);
745
746 assert_eq!(result.items.len(), 1);
747 assert_eq!(result.total_tokens, 100);
748 }
749
750 #[test]
751 fn test_context_result_add_multiple_items() {
752 let mut result = ContextResult::new("provider");
753 result.add_item(ContextItem::new("1", ContextType::Resource, "a").with_token_count(50));
754 result.add_item(ContextItem::new("2", ContextType::Memory, "b").with_token_count(75));
755 result.add_item(ContextItem::new("3", ContextType::Skill, "c").with_token_count(25));
756
757 assert_eq!(result.items.len(), 3);
758 assert_eq!(result.total_tokens, 150);
759 }
760
761 #[test]
762 fn context_result_token_accounting_saturates_on_overflow() {
763 let mut result = ContextResult::new("provider");
764 result.add_item(
765 ContextItem::new("large", ContextType::Resource, "large").with_token_count(usize::MAX),
766 );
767 result
768 .add_item(ContextItem::new("next", ContextType::Resource, "next").with_token_count(1));
769
770 assert_eq!(result.items.len(), 2);
771 assert_eq!(result.total_tokens, usize::MAX);
772 }
773
774 #[test]
775 fn bounded_context_file_read_rejects_growth_and_invalid_utf8() {
776 use std::io::{Seek, SeekFrom, Write};
777
778 let mut file = tempfile::NamedTempFile::new().unwrap();
779 write!(file, "12345").unwrap();
780 assert!(read_utf8_file_bounded(file.path(), 4).unwrap().is_none());
781 assert_eq!(
782 read_utf8_file_bounded(file.path(), 5).unwrap().as_deref(),
783 Some("12345")
784 );
785
786 file.as_file_mut().set_len(2).unwrap();
787 file.as_file_mut().seek(SeekFrom::Start(0)).unwrap();
788 file.as_file_mut().write_all(&[0xff, 0xfe]).unwrap();
789 assert!(read_utf8_file_bounded(file.path(), 2).unwrap().is_none());
790 }
791
792 #[test]
793 fn test_context_result_is_empty() {
794 let empty = ContextResult::new("provider");
795 assert!(empty.is_empty());
796
797 let mut non_empty = ContextResult::new("provider");
798 non_empty.add_item(ContextItem::new("id", ContextType::Resource, "content"));
799 assert!(!non_empty.is_empty());
800 }
801
802 #[test]
803 fn test_context_result_to_xml() {
804 let mut result = ContextResult::new("provider");
805 result.add_item(
806 ContextItem::new("1", ContextType::Resource, "First content").with_source("source://1"),
807 );
808 result.add_item(ContextItem::new("2", ContextType::Memory, "Second content"));
809
810 let xml = result.to_xml();
811 assert!(xml.contains("<context source=\"source://1\" type=\"Resource\">"));
812 assert!(xml.contains("First content"));
813 assert!(xml.contains("<context type=\"Memory\">"));
814 assert!(xml.contains("Second content"));
815 }
816
817 #[test]
818 fn test_context_result_to_xml_empty() {
819 let result = ContextResult::new("provider");
820 let xml = result.to_xml();
821 assert!(xml.is_empty());
822 }
823
824 #[test]
825 fn test_context_result_serialization() {
826 let mut result = ContextResult::new("test-provider");
827 result.truncated = true;
828 result.add_item(ContextItem::new("id", ContextType::Resource, "content"));
829
830 let json = serde_json::to_string(&result).unwrap();
831 let parsed: ContextResult = serde_json::from_str(&json).unwrap();
832
833 assert_eq!(parsed.provider, "test-provider");
834 assert!(parsed.truncated);
835 assert_eq!(parsed.items.len(), 1);
836 }
837
838 #[test]
839 fn test_context_result_default() {
840 let result: ContextResult = Default::default();
841 assert!(result.items.is_empty());
842 assert_eq!(result.total_tokens, 0);
843 assert!(result.provider.is_empty());
844 assert!(!result.truncated);
845 }
846
847 struct MockContextProvider {
852 name: String,
853 items: Vec<ContextItem>,
854 }
855
856 impl MockContextProvider {
857 fn new(name: &str) -> Self {
858 Self {
859 name: name.to_string(),
860 items: Vec::new(),
861 }
862 }
863
864 fn with_items(mut self, items: Vec<ContextItem>) -> Self {
865 self.items = items;
866 self
867 }
868 }
869
870 #[async_trait::async_trait]
871 impl ContextProvider for MockContextProvider {
872 fn name(&self) -> &str {
873 &self.name
874 }
875
876 async fn query(&self, _query: &ContextQuery) -> anyhow::Result<ContextResult> {
877 let mut result = ContextResult::new(&self.name);
878 for item in &self.items {
879 result.add_item(item.clone());
880 }
881 Ok(result)
882 }
883 }
884
885 #[tokio::test]
886 async fn test_mock_context_provider() {
887 let provider = MockContextProvider::new("mock").with_items(vec![ContextItem::new(
888 "1",
889 ContextType::Resource,
890 "content",
891 )]);
892
893 assert_eq!(provider.name(), "mock");
894
895 let query = ContextQuery::new("test");
896 let result = provider.query(&query).await.unwrap();
897
898 assert_eq!(result.provider, "mock");
899 assert_eq!(result.items.len(), 1);
900 }
901
902 #[tokio::test]
903 async fn test_context_provider_on_turn_complete_default() {
904 let provider = MockContextProvider::new("mock");
905
906 let result = provider
908 .on_turn_complete("session-1", "prompt", "response")
909 .await;
910 assert!(result.is_ok());
911 }
912
913 struct MockMemoryProvider {
914 memories: std::sync::Arc<tokio::sync::RwLock<Vec<(String, String, String)>>>,
915 }
916
917 impl MockMemoryProvider {
918 fn new() -> Self {
919 Self {
920 memories: std::sync::Arc::new(tokio::sync::RwLock::new(Vec::new())),
921 }
922 }
923 }
924
925 #[async_trait::async_trait]
926 impl ContextProvider for MockMemoryProvider {
927 fn name(&self) -> &str {
928 "memory-provider"
929 }
930
931 async fn query(&self, _query: &ContextQuery) -> anyhow::Result<ContextResult> {
932 Ok(ContextResult::new("memory-provider"))
933 }
934
935 async fn on_turn_complete(
936 &self,
937 session_id: &str,
938 prompt: &str,
939 response: &str,
940 ) -> anyhow::Result<()> {
941 let mut memories = self.memories.write().await;
942 memories.push((
943 session_id.to_string(),
944 prompt.to_string(),
945 response.to_string(),
946 ));
947 Ok(())
948 }
949 }
950
951 #[tokio::test]
952 async fn test_context_provider_on_turn_complete_custom() {
953 let provider = MockMemoryProvider::new();
954
955 provider
956 .on_turn_complete("sess-1", "What is Rust?", "Rust is a systems language.")
957 .await
958 .unwrap();
959
960 let memories = provider.memories.read().await;
961 assert_eq!(memories.len(), 1);
962 assert_eq!(memories[0].0, "sess-1");
963 assert_eq!(memories[0].1, "What is Rust?");
964 assert_eq!(memories[0].2, "Rust is a systems language.");
965 }
966
967 #[tokio::test]
972 async fn test_multiple_providers_query() {
973 let provider1 = MockContextProvider::new("provider-1").with_items(vec![ContextItem::new(
974 "p1-1",
975 ContextType::Resource,
976 "Resource from P1",
977 )]);
978
979 let provider2 = MockContextProvider::new("provider-2").with_items(vec![
980 ContextItem::new("p2-1", ContextType::Memory, "Memory from P2"),
981 ContextItem::new("p2-2", ContextType::Skill, "Skill from P2"),
982 ]);
983
984 let providers: Vec<&dyn ContextProvider> = vec![&provider1, &provider2];
985 let query = ContextQuery::new("test");
986
987 let mut all_items = Vec::new();
988 for provider in providers {
989 let result = provider.query(&query).await.unwrap();
990 all_items.extend(result.items);
991 }
992
993 assert_eq!(all_items.len(), 3);
994 assert!(all_items.iter().any(|i| i.id == "p1-1"));
995 assert!(all_items.iter().any(|i| i.id == "p2-1"));
996 assert!(all_items.iter().any(|i| i.id == "p2-2"));
997 }
998
999 #[test]
1000 fn test_context_result_xml_formatting_complex() {
1001 let mut result = ContextResult::new("openviking");
1002 result.add_item(
1003 ContextItem::new(
1004 "doc-1",
1005 ContextType::Resource,
1006 "Authentication uses JWT tokens stored in httpOnly cookies.",
1007 )
1008 .with_source("viking://docs/auth")
1009 .with_token_count(50),
1010 );
1011 result.add_item(
1012 ContextItem::new(
1013 "mem-1",
1014 ContextType::Memory,
1015 "User prefers TypeScript over JavaScript.",
1016 )
1017 .with_token_count(30),
1018 );
1019
1020 let xml = result.to_xml();
1021
1022 assert!(xml.contains("<context source=\"viking://docs/auth\" type=\"Resource\">"));
1024 assert!(xml.contains("Authentication uses JWT tokens"));
1025 assert!(xml.contains("<context type=\"Memory\">"));
1026 assert!(xml.contains("User prefers TypeScript"));
1027
1028 assert!(xml.contains("</context>\n\n<context"));
1030 }
1031}