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