1use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7
8use crate::error::MagiError;
9use crate::schema::{AgentName, AgentOutput, Finding, Severity, Verdict};
10
11#[non_exhaustive]
13#[derive(Debug, Clone)]
14pub struct ConsensusConfig {
15 pub min_agents: usize,
17 pub epsilon: f64,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ConsensusResult {
24 pub consensus: String,
26 pub consensus_verdict: Verdict,
28 pub confidence: f64,
30 pub score: f64,
32 pub agent_count: usize,
34 pub votes: BTreeMap<AgentName, Verdict>,
36 pub majority_summary: String,
38 pub dissent: Vec<Dissent>,
40 pub findings: Vec<DedupFinding>,
42 pub conditions: Vec<Condition>,
44 pub recommendations: BTreeMap<AgentName, String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
50pub struct DedupFinding {
51 pub severity: Severity,
53 pub title: String,
55 pub detail: String,
57 pub sources: Vec<AgentName>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63pub struct Dissent {
64 pub agent: AgentName,
66 pub summary: String,
68 pub reasoning: String,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
74pub struct Condition {
75 pub agent: AgentName,
77 pub condition: String,
79}
80
81impl Default for ConsensusConfig {
82 fn default() -> Self {
83 Self {
84 min_agents: 2,
85 epsilon: 1e-9,
86 }
87 }
88}
89
90pub struct ConsensusEngine {
99 config: ConsensusConfig,
100}
101
102impl ConsensusEngine {
103 pub fn min_agents(&self) -> usize {
105 self.config.min_agents
106 }
107
108 pub fn new(config: ConsensusConfig) -> Self {
112 let min_agents = if config.min_agents == 0 {
113 1
114 } else {
115 config.min_agents
116 };
117 Self {
118 config: ConsensusConfig {
119 min_agents,
120 ..config
121 },
122 }
123 }
124
125 pub fn determine(&self, agents: &[AgentOutput]) -> Result<ConsensusResult, MagiError> {
132 if agents.len() < self.config.min_agents {
134 return Err(MagiError::InsufficientAgents {
135 succeeded: agents.len(),
136 required: self.config.min_agents,
137 });
138 }
139
140 let mut seen = std::collections::HashSet::new();
142 for agent in agents {
143 if !seen.insert(agent.agent) {
144 return Err(MagiError::Validation(format!(
145 "duplicate agent name: {}",
146 agent.agent.display_name()
147 )));
148 }
149 }
150
151 let n = agents.len() as f64;
152 let epsilon = self.config.epsilon;
153
154 let score: f64 = agents.iter().map(|a| a.verdict.weight()).sum::<f64>() / n;
156
157 let approve_count = agents
159 .iter()
160 .filter(|a| a.effective_verdict() == Verdict::Approve)
161 .count();
162 let reject_count = agents.len() - approve_count;
163
164 let has_conditional = agents.iter().any(|a| a.verdict == Verdict::Conditional);
165
166 let majority_verdict = match approve_count.cmp(&reject_count) {
167 std::cmp::Ordering::Greater => Verdict::Approve,
168 std::cmp::Ordering::Less => Verdict::Reject,
169 std::cmp::Ordering::Equal => {
170 let first_approve = agents
172 .iter()
173 .filter(|a| a.effective_verdict() == Verdict::Approve)
174 .map(|a| a.agent)
175 .min();
176 let first_reject = agents
177 .iter()
178 .filter(|a| a.effective_verdict() == Verdict::Reject)
179 .map(|a| a.agent)
180 .min();
181 match (first_approve, first_reject) {
182 (Some(a), Some(r)) if a < r => Verdict::Approve,
183 (Some(_), None) => Verdict::Approve,
184 _ => Verdict::Reject,
185 }
186 }
187 };
188
189 let (mut label, consensus_verdict) =
191 self.classify(score, epsilon, approve_count, reject_count, has_conditional);
192
193 if agents.len() < 3 {
195 if label == "STRONG GO" {
196 label = format!("GO ({}-0)", agents.len());
197 } else if label == "STRONG NO-GO" {
198 label = format!("HOLD ({}-0)", agents.len());
199 }
200 }
201
202 let base_confidence: f64 = agents
210 .iter()
211 .filter(|a| a.effective_verdict() == majority_verdict)
212 .map(|a| a.confidence)
213 .sum::<f64>()
214 / n;
215 let weight_factor = (score.abs() + 1.0) / 2.0;
216 let confidence = (base_confidence * weight_factor).clamp(0.0, 1.0);
217 let confidence = (confidence * 100.0).round() / 100.0;
218
219 let findings = self.deduplicate_findings(agents);
221
222 let dissent: Vec<Dissent> = agents
224 .iter()
225 .filter(|a| a.effective_verdict() != majority_verdict)
226 .map(|a| Dissent {
227 agent: a.agent,
228 summary: a.summary.clone(),
229 reasoning: a.reasoning.clone(),
230 })
231 .collect();
232
233 let conditions: Vec<Condition> = agents
235 .iter()
236 .filter(|a| a.verdict == Verdict::Conditional)
237 .map(|a| Condition {
238 agent: a.agent,
239 condition: a.recommendation.clone(),
240 })
241 .collect();
242
243 let votes: BTreeMap<AgentName, Verdict> =
245 agents.iter().map(|a| (a.agent, a.verdict)).collect();
246
247 let majority_summary = agents
249 .iter()
250 .filter(|a| a.effective_verdict() == majority_verdict)
251 .map(|a| a.summary.as_str())
252 .collect::<Vec<_>>()
253 .join(" | ");
254
255 let recommendations: BTreeMap<AgentName, String> = agents
257 .iter()
258 .map(|a| (a.agent, a.recommendation.clone()))
259 .collect();
260
261 Ok(ConsensusResult {
262 consensus: label,
263 consensus_verdict,
264 confidence,
265 score,
266 agent_count: agents.len(),
267 votes,
268 majority_summary,
269 dissent,
270 findings,
271 conditions,
272 recommendations,
273 })
274 }
275
276 fn classify(
278 &self,
279 score: f64,
280 epsilon: f64,
281 approve_count: usize,
282 reject_count: usize,
283 has_conditional: bool,
284 ) -> (String, Verdict) {
285 if (score - 1.0).abs() < epsilon {
286 ("STRONG GO".to_string(), Verdict::Approve)
287 } else if (score - (-1.0)).abs() < epsilon {
288 ("STRONG NO-GO".to_string(), Verdict::Reject)
289 } else if score > epsilon && has_conditional {
290 ("GO WITH CAVEATS".to_string(), Verdict::Approve)
291 } else if score > epsilon {
292 (
293 format!("GO ({}-{})", approve_count, reject_count),
294 Verdict::Approve,
295 )
296 } else if score.abs() < epsilon {
297 ("HOLD -- TIE".to_string(), Verdict::Reject)
298 } else {
299 (
300 format!("HOLD ({}-{})", reject_count, approve_count),
301 Verdict::Reject,
302 )
303 }
304 }
305
306 fn deduplicate_findings(&self, agents: &[AgentOutput]) -> Vec<DedupFinding> {
308 let mut agent_findings: Vec<(AgentName, &Finding)> = Vec::new();
310 for agent in agents {
311 for finding in &agent.findings {
312 agent_findings.push((agent.agent, finding));
313 }
314 }
315 agent_findings.sort_by(|a, b| a.0.cmp(&b.0));
317
318 let mut groups: std::collections::HashMap<String, Vec<(AgentName, &Finding)>> =
320 std::collections::HashMap::new();
321 let mut order: Vec<String> = Vec::new();
322
323 for (agent_name, finding) in &agent_findings {
324 let key = finding
325 .stripped_title()
326 .split_whitespace()
327 .collect::<Vec<_>>()
328 .join(" ")
329 .to_lowercase();
330 if !groups.contains_key(&key) {
331 order.push(key.clone());
332 }
333 groups.entry(key).or_default().push((*agent_name, finding));
334 }
335
336 let mut result: Vec<DedupFinding> = Vec::new();
337 for key in &order {
338 let entries = &groups[key];
339 let max_severity = entries.iter().map(|(_, f)| f.severity).max().unwrap();
341 let best = entries
343 .iter()
344 .find(|(_, f)| f.severity == max_severity)
345 .unwrap();
346 let sources: Vec<AgentName> = entries.iter().map(|(name, _)| *name).collect();
347 result.push(DedupFinding {
348 severity: max_severity,
349 title: best.1.title.clone(),
350 detail: best.1.detail.clone(),
351 sources,
352 });
353 }
354
355 result.sort_by(|a, b| b.severity.cmp(&a.severity));
357 result
358 }
359}
360
361impl Default for ConsensusEngine {
362 fn default() -> Self {
363 Self::new(ConsensusConfig::default())
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use crate::schema::*;
371
372 fn make_output(agent: AgentName, verdict: Verdict, confidence: f64) -> AgentOutput {
373 AgentOutput {
374 agent,
375 verdict,
376 confidence,
377 summary: format!("{} summary", agent.display_name()),
378 reasoning: format!("{} reasoning", agent.display_name()),
379 findings: vec![],
380 recommendation: format!("{} recommendation", agent.display_name()),
381 }
382 }
383
384 #[test]
388 fn test_unanimous_approve_produces_strong_go() {
389 let agents = vec![
390 make_output(AgentName::Melchior, Verdict::Approve, 0.9),
391 make_output(AgentName::Balthasar, Verdict::Approve, 0.9),
392 make_output(AgentName::Caspar, Verdict::Approve, 0.9),
393 ];
394 let engine = ConsensusEngine::new(ConsensusConfig::default());
395 let result = engine.determine(&agents).unwrap();
396 assert_eq!(result.consensus, "STRONG GO");
397 assert_eq!(result.consensus_verdict, Verdict::Approve);
398 assert!((result.score - 1.0).abs() < 1e-9);
399 }
400
401 #[test]
405 fn test_two_approve_one_reject_produces_go_2_1() {
406 let agents = vec![
407 make_output(AgentName::Melchior, Verdict::Approve, 0.9),
408 make_output(AgentName::Balthasar, Verdict::Approve, 0.8),
409 make_output(AgentName::Caspar, Verdict::Reject, 0.7),
410 ];
411 let engine = ConsensusEngine::new(ConsensusConfig::default());
412 let result = engine.determine(&agents).unwrap();
413 assert_eq!(result.consensus, "GO (2-1)");
414 assert_eq!(result.consensus_verdict, Verdict::Approve);
415 assert!(result.score > 0.0);
416 assert_eq!(result.dissent.len(), 1);
417 assert_eq!(result.dissent[0].agent, AgentName::Caspar);
418 }
419
420 #[test]
424 fn test_approve_conditional_reject_produces_go_with_caveats() {
425 let agents = vec![
426 make_output(AgentName::Melchior, Verdict::Approve, 0.9),
427 make_output(AgentName::Balthasar, Verdict::Conditional, 0.8),
428 make_output(AgentName::Caspar, Verdict::Reject, 0.7),
429 ];
430 let engine = ConsensusEngine::new(ConsensusConfig::default());
431 let result = engine.determine(&agents).unwrap();
432 assert_eq!(result.consensus, "GO WITH CAVEATS");
433 assert_eq!(result.consensus_verdict, Verdict::Approve);
434 assert!(!result.conditions.is_empty());
435 assert_eq!(result.conditions[0].agent, AgentName::Balthasar);
436 }
437
438 #[test]
442 fn test_unanimous_reject_produces_strong_no_go() {
443 let agents = vec![
444 make_output(AgentName::Melchior, Verdict::Reject, 0.9),
445 make_output(AgentName::Balthasar, Verdict::Reject, 0.8),
446 make_output(AgentName::Caspar, Verdict::Reject, 0.7),
447 ];
448 let engine = ConsensusEngine::new(ConsensusConfig::default());
449 let result = engine.determine(&agents).unwrap();
450 assert_eq!(result.consensus, "STRONG NO-GO");
451 assert_eq!(result.consensus_verdict, Verdict::Reject);
452 assert!((result.score - (-1.0)).abs() < 1e-9);
453 }
454
455 #[test]
459 fn test_tie_with_two_agents_produces_hold_tie() {
460 let agents = vec![
461 make_output(AgentName::Melchior, Verdict::Approve, 0.9),
462 make_output(AgentName::Caspar, Verdict::Reject, 0.9),
463 ];
464 let engine = ConsensusEngine::new(ConsensusConfig::default());
465 let result = engine.determine(&agents).unwrap();
466 assert_eq!(result.consensus, "HOLD -- TIE");
467 assert_eq!(result.consensus_verdict, Verdict::Reject);
468 }
469
470 #[test]
474 fn test_duplicate_findings_merged_with_severity_promoted() {
475 let mut m = make_output(AgentName::Melchior, Verdict::Approve, 0.9);
476 m.findings.push(Finding {
477 severity: Severity::Warning,
478 title: "Security Issue".to_string(),
479 detail: "detail_warning".to_string(),
480 });
481 let mut b = make_output(AgentName::Balthasar, Verdict::Approve, 0.9);
482 b.findings.push(Finding {
483 severity: Severity::Critical,
484 title: "security issue".to_string(),
485 detail: "detail_critical".to_string(),
486 });
487 let c = make_output(AgentName::Caspar, Verdict::Approve, 0.9);
488 let engine = ConsensusEngine::new(ConsensusConfig::default());
489 let result = engine.determine(&[m, b, c]).unwrap();
490 assert_eq!(result.findings.len(), 1);
491 assert_eq!(result.findings[0].severity, Severity::Critical);
492 }
493
494 #[test]
496 fn test_merged_finding_sources_include_both_agents() {
497 let mut m = make_output(AgentName::Melchior, Verdict::Approve, 0.9);
498 m.findings.push(Finding {
499 severity: Severity::Warning,
500 title: "Security Issue".to_string(),
501 detail: "detail_m".to_string(),
502 });
503 let mut b = make_output(AgentName::Balthasar, Verdict::Approve, 0.9);
504 b.findings.push(Finding {
505 severity: Severity::Critical,
506 title: "security issue".to_string(),
507 detail: "detail_b".to_string(),
508 });
509 let c = make_output(AgentName::Caspar, Verdict::Approve, 0.9);
510 let engine = ConsensusEngine::new(ConsensusConfig::default());
511 let result = engine.determine(&[m, b, c]).unwrap();
512 assert_eq!(result.findings[0].sources.len(), 2);
513 assert!(result.findings[0].sources.contains(&AgentName::Melchior));
514 assert!(result.findings[0].sources.contains(&AgentName::Balthasar));
515 }
516
517 #[test]
519 fn test_merged_finding_detail_from_highest_severity() {
520 let mut m = make_output(AgentName::Melchior, Verdict::Approve, 0.9);
521 m.findings.push(Finding {
522 severity: Severity::Warning,
523 title: "Issue".to_string(),
524 detail: "detail_warning".to_string(),
525 });
526 let mut b = make_output(AgentName::Balthasar, Verdict::Approve, 0.9);
527 b.findings.push(Finding {
528 severity: Severity::Critical,
529 title: "issue".to_string(),
530 detail: "detail_critical".to_string(),
531 });
532 let c = make_output(AgentName::Caspar, Verdict::Approve, 0.9);
533 let engine = ConsensusEngine::new(ConsensusConfig::default());
534 let result = engine.determine(&[m, b, c]).unwrap();
535 assert_eq!(result.findings[0].detail, "detail_critical");
536 }
537
538 #[test]
540 fn test_merged_finding_detail_from_first_agent_on_same_severity() {
541 let mut b = make_output(AgentName::Balthasar, Verdict::Approve, 0.9);
542 b.findings.push(Finding {
543 severity: Severity::Warning,
544 title: "Issue".to_string(),
545 detail: "detail_b".to_string(),
546 });
547 let mut m = make_output(AgentName::Melchior, Verdict::Approve, 0.9);
548 m.findings.push(Finding {
549 severity: Severity::Warning,
550 title: "issue".to_string(),
551 detail: "detail_m".to_string(),
552 });
553 let c = make_output(AgentName::Caspar, Verdict::Approve, 0.9);
554 let engine = ConsensusEngine::new(ConsensusConfig::default());
555 let result = engine.determine(&[b, m, c]).unwrap();
556 assert_eq!(result.findings[0].detail, "detail_b");
558 }
559
560 #[test]
564 fn test_degraded_mode_caps_strong_go_to_go() {
565 let agents = vec![
566 make_output(AgentName::Melchior, Verdict::Approve, 0.9),
567 make_output(AgentName::Balthasar, Verdict::Approve, 0.9),
568 ];
569 let engine = ConsensusEngine::new(ConsensusConfig::default());
570 let result = engine.determine(&agents).unwrap();
571 assert_eq!(result.consensus, "GO (2-0)");
572 assert_ne!(result.consensus, "STRONG GO");
573 }
574
575 #[test]
577 fn test_degraded_mode_caps_strong_no_go_to_hold() {
578 let agents = vec![
579 make_output(AgentName::Melchior, Verdict::Reject, 0.9),
580 make_output(AgentName::Balthasar, Verdict::Reject, 0.9),
581 ];
582 let engine = ConsensusEngine::new(ConsensusConfig::default());
583 let result = engine.determine(&agents).unwrap();
584 assert_eq!(result.consensus, "HOLD (2-0)");
585 assert_ne!(result.consensus, "STRONG NO-GO");
586 }
587
588 #[test]
592 fn test_determine_rejects_fewer_than_min_agents() {
593 let agents = vec![make_output(AgentName::Melchior, Verdict::Approve, 0.9)];
594 let engine = ConsensusEngine::new(ConsensusConfig::default());
595 let result = engine.determine(&agents);
596 assert!(result.is_err());
597 let err = result.unwrap_err();
598 assert!(matches!(
599 err,
600 MagiError::InsufficientAgents {
601 succeeded: 1,
602 required: 2,
603 }
604 ));
605 }
606
607 #[test]
609 fn test_determine_rejects_duplicate_agent_names() {
610 let agents = vec![
611 make_output(AgentName::Melchior, Verdict::Approve, 0.9),
612 make_output(AgentName::Melchior, Verdict::Reject, 0.8),
613 ];
614 let engine = ConsensusEngine::new(ConsensusConfig::default());
615 let result = engine.determine(&agents);
616 assert!(result.is_err());
617 assert!(matches!(result.unwrap_err(), MagiError::Validation(_)));
618 }
619
620 #[test]
624 fn test_epsilon_aware_classification_near_boundaries() {
625 let agents = vec![
630 make_output(AgentName::Melchior, Verdict::Approve, 0.9),
631 make_output(AgentName::Balthasar, Verdict::Reject, 0.9),
632 ];
633 let engine = ConsensusEngine::new(ConsensusConfig::default());
634 let result = engine.determine(&agents).unwrap();
635 assert_eq!(result.consensus, "HOLD -- TIE");
636 }
637
638 #[test]
640 fn test_confidence_formula_clamped_and_rounded() {
641 let agents = vec![
647 make_output(AgentName::Melchior, Verdict::Approve, 0.9),
648 make_output(AgentName::Balthasar, Verdict::Approve, 0.9),
649 make_output(AgentName::Caspar, Verdict::Approve, 0.9),
650 ];
651 let engine = ConsensusEngine::new(ConsensusConfig::default());
652 let result = engine.determine(&agents).unwrap();
653 assert!((result.confidence - 0.9).abs() < 1e-9);
654 }
655
656 #[test]
658 fn test_confidence_with_mixed_verdicts() {
659 let agents = vec![
666 make_output(AgentName::Melchior, Verdict::Approve, 0.9),
667 make_output(AgentName::Balthasar, Verdict::Approve, 0.8),
668 make_output(AgentName::Caspar, Verdict::Reject, 0.7),
669 ];
670 let engine = ConsensusEngine::new(ConsensusConfig::default());
671 let result = engine.determine(&agents).unwrap();
672 assert!((result.confidence - 0.38).abs() < 0.01);
673 }
674
675 #[test]
677 fn test_majority_summary_joins_with_pipe() {
678 let agents = vec![
679 make_output(AgentName::Melchior, Verdict::Approve, 0.9),
680 make_output(AgentName::Balthasar, Verdict::Approve, 0.8),
681 make_output(AgentName::Caspar, Verdict::Reject, 0.7),
682 ];
683 let engine = ConsensusEngine::new(ConsensusConfig::default());
684 let result = engine.determine(&agents).unwrap();
685 assert!(result.majority_summary.contains("Melchior summary"));
686 assert!(result.majority_summary.contains("Balthasar summary"));
687 assert!(result.majority_summary.contains(" | "));
688 assert!(!result.majority_summary.contains("Caspar summary"));
689 }
690
691 #[test]
693 fn test_conditions_extracted_from_conditional_agents() {
694 let agents = vec![
695 make_output(AgentName::Melchior, Verdict::Approve, 0.9),
696 make_output(AgentName::Balthasar, Verdict::Conditional, 0.8),
697 make_output(AgentName::Caspar, Verdict::Reject, 0.7),
698 ];
699 let engine = ConsensusEngine::new(ConsensusConfig::default());
700 let result = engine.determine(&agents).unwrap();
701 assert_eq!(result.conditions.len(), 1);
702 assert_eq!(result.conditions[0].agent, AgentName::Balthasar);
703 assert_eq!(result.conditions[0].condition, "Balthasar recommendation");
704 }
705
706 #[test]
708 fn test_recommendations_includes_all_agents() {
709 let agents = vec![
710 make_output(AgentName::Melchior, Verdict::Approve, 0.9),
711 make_output(AgentName::Balthasar, Verdict::Approve, 0.8),
712 make_output(AgentName::Caspar, Verdict::Reject, 0.7),
713 ];
714 let engine = ConsensusEngine::new(ConsensusConfig::default());
715 let result = engine.determine(&agents).unwrap();
716 assert_eq!(result.recommendations.len(), 3);
717 assert!(result.recommendations.contains_key(&AgentName::Melchior));
718 assert!(result.recommendations.contains_key(&AgentName::Balthasar));
719 assert!(result.recommendations.contains_key(&AgentName::Caspar));
720 }
721
722 #[test]
724 fn test_consensus_config_enforces_min_agents_at_least_one() {
725 let config = ConsensusConfig {
726 min_agents: 0,
727 ..ConsensusConfig::default()
728 };
729 assert_eq!(config.min_agents, 0);
730 let engine = ConsensusEngine::new(config);
732 let agents = vec![make_output(AgentName::Melchior, Verdict::Approve, 0.9)];
733 let result = engine.determine(&agents);
735 assert!(result.is_ok());
736 }
737
738 #[test]
740 fn test_consensus_config_default_values() {
741 let config = ConsensusConfig::default();
742 assert_eq!(config.min_agents, 2);
743 assert!((config.epsilon - 1e-9).abs() < 1e-15);
744 }
745
746 #[test]
748 fn test_tiebreak_by_agent_name_ordering() {
749 let agents = vec![
753 make_output(AgentName::Balthasar, Verdict::Approve, 0.9),
754 make_output(AgentName::Melchior, Verdict::Reject, 0.9),
755 ];
756 let engine = ConsensusEngine::new(ConsensusConfig::default());
757 let result = engine.determine(&agents).unwrap();
758 assert_eq!(result.consensus, "HOLD -- TIE");
760 assert_eq!(result.consensus_verdict, Verdict::Reject);
761 }
762
763 #[test]
765 fn test_findings_sorted_by_severity_critical_first() {
766 let mut m = make_output(AgentName::Melchior, Verdict::Approve, 0.9);
767 m.findings.push(Finding {
768 severity: Severity::Info,
769 title: "Info issue".to_string(),
770 detail: "info detail".to_string(),
771 });
772 m.findings.push(Finding {
773 severity: Severity::Critical,
774 title: "Critical issue".to_string(),
775 detail: "critical detail".to_string(),
776 });
777 let b = make_output(AgentName::Balthasar, Verdict::Approve, 0.9);
778 let c = make_output(AgentName::Caspar, Verdict::Approve, 0.9);
779 let engine = ConsensusEngine::new(ConsensusConfig::default());
780 let result = engine.determine(&[m, b, c]).unwrap();
781 assert_eq!(result.findings.len(), 2);
782 assert_eq!(result.findings[0].severity, Severity::Critical);
783 assert_eq!(result.findings[1].severity, Severity::Info);
784 }
785
786 #[test]
788 fn test_votes_map_contains_all_agents() {
789 let agents = vec![
790 make_output(AgentName::Melchior, Verdict::Approve, 0.9),
791 make_output(AgentName::Balthasar, Verdict::Reject, 0.8),
792 make_output(AgentName::Caspar, Verdict::Conditional, 0.7),
793 ];
794 let engine = ConsensusEngine::new(ConsensusConfig::default());
795 let result = engine.determine(&agents).unwrap();
796 assert_eq!(result.votes.len(), 3);
797 assert_eq!(result.votes[&AgentName::Melchior], Verdict::Approve);
798 assert_eq!(result.votes[&AgentName::Balthasar], Verdict::Reject);
799 assert_eq!(result.votes[&AgentName::Caspar], Verdict::Conditional);
800 }
801
802 #[test]
804 fn test_agent_count_reflects_input_count() {
805 let agents = vec![
806 make_output(AgentName::Melchior, Verdict::Approve, 0.9),
807 make_output(AgentName::Balthasar, Verdict::Approve, 0.8),
808 make_output(AgentName::Caspar, Verdict::Approve, 0.7),
809 ];
810 let engine = ConsensusEngine::new(ConsensusConfig::default());
811 let result = engine.determine(&agents).unwrap();
812 assert_eq!(result.agent_count, 3);
813 }
814}