lean_ctx/core/context_kernel/
multi_agent_e2e.rs1#[cfg(test)]
2mod tests {
3 use std::collections::HashMap;
4
5 use super::super::capsule_wire::{self, ContextCapsuleV1};
6 use super::super::etpao::{EtpaoMetrics, EtpaoTracker};
7 use super::super::knowledge_health;
8 use super::super::result_fusion::{self, ChildResult, FusionStrategy};
9 use super::super::types::{ContextReceiptV1, ReceiptOutcome};
10
11 fn receipt(id: &str, delivered_tokens: usize, cache_hits: usize) -> ContextReceiptV1 {
12 ContextReceiptV1 {
13 receipt_id: format!("receipt:{id}"),
14 plan_id: format!("plan:{id}"),
15 delivered_tokens,
16 cache_hits,
17 cache_misses: usize::from(cache_hits == 0),
18 outcome: ReceiptOutcome::Accepted,
19 quality_signals: Vec::new(),
20 feedback_attribution: HashMap::new(),
21 }
22 }
23
24 fn capsule(owner: &str, refs: Vec<String>, budget: u64) -> ContextCapsuleV1 {
25 ContextCapsuleV1::new("multi-agent-e2e", refs, budget, owner)
26 }
27
28 fn child_result(
29 agent_id: &str,
30 evidence: &[&str],
31 confidence: f64,
32 quality_score: f64,
33 contradicts: &[&str],
34 ) -> ChildResult {
35 ChildResult {
36 agent_id: agent_id.to_owned(),
37 receipt_ref: format!("receipt:{agent_id}"),
38 evidence: evidence.iter().map(|item| (*item).to_owned()).collect(),
39 confidence,
40 quality_score,
41 contradicts: contradicts
42 .iter()
43 .map(|agent| (*agent).to_owned())
44 .collect(),
45 }
46 }
47
48 #[test]
49 fn three_deep_chain_budget_cascade() {
50 let mut tracker = EtpaoTracker::default();
51 let mut parent = capsule("parent", vec!["ref:root".to_owned()], 10_000);
52
53 let mut child = capsule("child", vec!["ref:child".to_owned()], 5_000);
54 child.parent_capsule = Some(parent.capsule_id.clone());
55 parent.budget_remaining = parent.budget_remaining.saturating_sub(child.budget_tokens);
56 tracker.record_receipt(
57 "parent",
58 &receipt("parent-to-child", 5_000, 0),
59 ReceiptOutcome::Accepted,
60 );
61
62 let mut grandchild = capsule("grandchild", vec!["ref:grandchild".to_owned()], 2_500);
63 grandchild.parent_capsule = Some(child.capsule_id.clone());
64 child.budget_remaining = child
65 .budget_remaining
66 .saturating_sub(grandchild.budget_tokens);
67 tracker.record_receipt(
68 "child",
69 &receipt("child-to-grandchild", 2_500, 0),
70 ReceiptOutcome::Accepted,
71 );
72
73 assert_eq!(parent.budget_remaining, 5_000);
74 assert_eq!(child.budget_remaining, 2_500);
75 assert_eq!(grandchild.budget_remaining, 2_500);
76 assert!(grandchild.budget_remaining <= child.budget_remaining);
77 assert!(child.budget_remaining <= parent.budget_remaining);
78 assert_eq!(
79 tracker.get("parent").map(|metrics| metrics.tokens_input),
80 Some(5_000)
81 );
82 assert_eq!(
83 tracker.get("child").map(|metrics| metrics.tokens_input),
84 Some(2_500)
85 );
86 }
87
88 #[test]
89 fn sibling_context_dedup() {
90 let shared = ["ref:A", "ref:B"];
91 let siblings: Vec<_> = ["ref:one", "ref:two", "ref:three"]
92 .into_iter()
93 .enumerate()
94 .map(|(index, unique)| {
95 capsule(
96 &format!("sibling-{index}"),
97 shared
98 .into_iter()
99 .chain([unique])
100 .map(str::to_owned)
101 .collect(),
102 1_000,
103 )
104 })
105 .collect();
106
107 let report = capsule_wire::dedup_siblings(&siblings);
108
109 assert_eq!(report.shared_refs, vec!["ref:A", "ref:B"]);
110 assert!(report.dedup_ratio > 0.0);
111 assert!(report.unique_per_capsule.iter().all(|refs| refs.len() == 1));
112 }
113
114 #[test]
115 fn delta_transfer_smaller_than_full() {
116 let original_refs: Vec<_> = (0..10)
117 .map(|index| format!("ref:original-context-object-{index:02}-with-stable-metadata"))
118 .collect();
119 let base = capsule("parent", original_refs.clone(), 5_000);
120 let mut updated_refs = original_refs[..8].to_vec();
121 updated_refs.extend([
122 "ref:new-context-object-10-with-stable-metadata".to_owned(),
123 "ref:new-context-object-11-with-stable-metadata".to_owned(),
124 ]);
125 let mut updated = capsule("parent", updated_refs, 5_000);
126 updated.created_at_epoch = base.created_at_epoch;
127
128 let delta = capsule_wire::compute_delta(&base, &updated);
129 let delta_size = serde_json::to_vec(&delta).expect("delta serializes").len();
130 let full_size = serde_json::to_vec(&updated)
131 .expect("capsule serializes")
132 .len();
133 let applied = capsule_wire::apply_delta(&base, &delta);
134
135 assert!(delta_size * 2 < full_size);
136 assert_eq!(applied, updated);
137 }
138
139 #[test]
140 fn result_fusion_resolves_contradiction() {
141 let results = [
142 child_result("agent-a", &["finding:accepted"], 0.95, 0.95, &["agent-b"]),
143 child_result("agent-b", &["finding:rejected"], 0.80, 0.90, &["agent-a"]),
144 child_result("agent-c", &["finding:accepted"], 0.85, 0.90, &[]),
145 ];
146
147 let report = result_fusion::fuse_results(&results, FusionStrategy::WeightedMerge);
148
149 assert!(report.conflicts.iter().any(|conflict| {
150 (conflict.agent_a == "agent-a" && conflict.agent_b == "agent-b")
151 || (conflict.agent_a == "agent-b" && conflict.agent_b == "agent-a")
152 }));
153 assert_eq!(report.winning_agent, "agent-a");
154 assert!(
155 report
156 .merged_evidence
157 .contains(&"finding:accepted".to_owned())
158 );
159 }
160
161 #[test]
162 fn etpao_improves_with_context_reuse() {
163 let mut tracker = EtpaoTracker::default();
164 for index in 0..5 {
165 tracker.record_receipt(
166 "chain",
167 &receipt(&format!("uncached-{index}"), 1_000, 0),
168 ReceiptOutcome::Accepted,
169 );
170 }
171 let first: EtpaoMetrics = tracker.aggregate();
172 let first_etpao = first.etpao();
173
174 for index in 0..5 {
175 tracker.record_receipt(
176 "chain",
177 &receipt(&format!("cached-{index}"), 100, 1),
178 ReceiptOutcome::Accepted,
179 );
180 }
181 let second_etpao = tracker.aggregate().etpao();
182
183 assert!(second_etpao < first_etpao);
184 }
185
186 #[test]
187 fn knowledge_health_detects_stale_facts() {
188 let mut facts = vec![(true, false); 3];
189 facts.extend([(false, false); 5]);
190 facts.extend([(false, true); 2]);
191
192 let report = knowledge_health::assess_health(&facts, 0, 0, 0, 0);
193
194 assert!((report.freshness_score - 0.3).abs() < f64::EPSILON);
195 assert!((report.stale_ratio - 0.7).abs() < f64::EPSILON);
196 assert!((report.contradiction_rate - 0.2).abs() < f64::EPSILON);
197 }
198}