Skip to content

Commit ceb4984

Browse files
authored
Merge pull request #18 from cuteolaf/test/epoch
test(epoch): add comprehensive unit tests achieving 95%+ coverage
2 parents 5adb7ba + 13b55b1 commit ceb4984

File tree

5 files changed

+1429
-0
lines changed

5 files changed

+1429
-0
lines changed

crates/epoch/src/aggregator.rs

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,4 +290,281 @@ mod tests {
290290
let total: u64 = distribution.distributions.iter().map(|d| d.emission).sum();
291291
assert!(total <= 1000);
292292
}
293+
294+
#[test]
295+
fn test_no_active_challenges() {
296+
let aggregator = WeightAggregator::new(EpochConfig::default());
297+
298+
let mut challenge = create_test_challenge("Challenge", 0.5);
299+
challenge.is_active = false;
300+
301+
let distribution =
302+
aggregator.calculate_emissions(0, 1000, &[challenge], &HashMap::new());
303+
304+
assert_eq!(distribution.distributions.len(), 0);
305+
}
306+
307+
#[test]
308+
fn test_zero_emission_weight() {
309+
let aggregator = WeightAggregator::new(EpochConfig::default());
310+
311+
let challenge = create_test_challenge("Challenge", 0.0);
312+
313+
let distribution =
314+
aggregator.calculate_emissions(0, 1000, &[challenge], &HashMap::new());
315+
316+
assert_eq!(distribution.distributions.len(), 0);
317+
}
318+
319+
#[test]
320+
fn test_missing_finalized_weights() {
321+
let aggregator = WeightAggregator::new(EpochConfig::default());
322+
323+
let challenge = create_test_challenge("Challenge", 0.5);
324+
325+
// No finalized weights for this challenge
326+
let distribution =
327+
aggregator.calculate_emissions(0, 1000, &[challenge], &HashMap::new());
328+
329+
assert_eq!(distribution.distributions.len(), 0);
330+
}
331+
332+
#[test]
333+
fn test_merge_agent_emissions() {
334+
let aggregator = WeightAggregator::new(EpochConfig::default());
335+
336+
let challenge1 = create_test_challenge("Challenge1", 0.5);
337+
let challenge2 = create_test_challenge("Challenge2", 0.5);
338+
339+
let mut finalized = HashMap::new();
340+
341+
// Same agent in both challenges
342+
finalized.insert(
343+
challenge1.id,
344+
FinalizedWeights {
345+
challenge_id: challenge1.id,
346+
epoch: 0,
347+
weights: vec![WeightAssignment::new("agent1".to_string(), 1.0)],
348+
participating_validators: vec![],
349+
excluded_validators: vec![],
350+
smoothing_applied: 0.0,
351+
finalized_at: chrono::Utc::now(),
352+
},
353+
);
354+
355+
finalized.insert(
356+
challenge2.id,
357+
FinalizedWeights {
358+
challenge_id: challenge2.id,
359+
epoch: 0,
360+
weights: vec![WeightAssignment::new("agent1".to_string(), 1.0)],
361+
participating_validators: vec![],
362+
excluded_validators: vec![],
363+
smoothing_applied: 0.0,
364+
finalized_at: chrono::Utc::now(),
365+
},
366+
);
367+
368+
let distribution =
369+
aggregator.calculate_emissions(0, 1000, &[challenge1, challenge2], &finalized);
370+
371+
// agent1 should have merged emissions from both challenges
372+
assert_eq!(distribution.distributions.len(), 1);
373+
assert_eq!(distribution.distributions[0].hotkey, "agent1");
374+
assert!(distribution.distributions[0].emission > 0);
375+
}
376+
377+
#[test]
378+
fn test_detect_suspicious_validators() {
379+
let aggregator = WeightAggregator::new(EpochConfig::default());
380+
381+
let validator1 = Keypair::generate().hotkey();
382+
let validator2 = Keypair::generate().hotkey();
383+
384+
let finalized = vec![
385+
FinalizedWeights {
386+
challenge_id: ChallengeId::new(),
387+
epoch: 0,
388+
weights: vec![],
389+
participating_validators: vec![],
390+
excluded_validators: vec![validator1.clone(), validator2.clone()],
391+
smoothing_applied: 0.0,
392+
finalized_at: chrono::Utc::now(),
393+
},
394+
];
395+
396+
let suspicious = aggregator.detect_suspicious_validators(&finalized);
397+
assert_eq!(suspicious.len(), 2);
398+
assert!(suspicious.iter().any(|s| s.hotkey == validator1));
399+
assert!(suspicious.iter().any(|s| s.hotkey == validator2));
400+
}
401+
402+
#[test]
403+
fn test_validator_metrics_full_participation() {
404+
let aggregator = WeightAggregator::new(EpochConfig::default());
405+
406+
let validator = Keypair::generate().hotkey();
407+
408+
let history = vec![
409+
FinalizedWeights {
410+
challenge_id: ChallengeId::new(),
411+
epoch: 0,
412+
weights: vec![],
413+
participating_validators: vec![validator.clone()],
414+
excluded_validators: vec![],
415+
smoothing_applied: 0.0,
416+
finalized_at: chrono::Utc::now(),
417+
},
418+
FinalizedWeights {
419+
challenge_id: ChallengeId::new(),
420+
epoch: 1,
421+
weights: vec![],
422+
participating_validators: vec![validator.clone()],
423+
excluded_validators: vec![],
424+
smoothing_applied: 0.0,
425+
finalized_at: chrono::Utc::now(),
426+
},
427+
];
428+
429+
let metrics = aggregator.validator_metrics(&validator, &history);
430+
assert_eq!(metrics.epochs_participated, 2);
431+
assert_eq!(metrics.epochs_excluded, 0);
432+
assert_eq!(metrics.participation_rate, 1.0);
433+
}
434+
435+
#[test]
436+
fn test_validator_metrics_partial_participation() {
437+
let aggregator = WeightAggregator::new(EpochConfig::default());
438+
439+
let validator = Keypair::generate().hotkey();
440+
441+
let history = vec![
442+
FinalizedWeights {
443+
challenge_id: ChallengeId::new(),
444+
epoch: 0,
445+
weights: vec![],
446+
participating_validators: vec![validator.clone()],
447+
excluded_validators: vec![],
448+
smoothing_applied: 0.0,
449+
finalized_at: chrono::Utc::now(),
450+
},
451+
FinalizedWeights {
452+
challenge_id: ChallengeId::new(),
453+
epoch: 1,
454+
weights: vec![],
455+
participating_validators: vec![],
456+
excluded_validators: vec![validator.clone()],
457+
smoothing_applied: 0.0,
458+
finalized_at: chrono::Utc::now(),
459+
},
460+
];
461+
462+
let metrics = aggregator.validator_metrics(&validator, &history);
463+
assert_eq!(metrics.epochs_participated, 1);
464+
assert_eq!(metrics.epochs_excluded, 1);
465+
assert_eq!(metrics.participation_rate, 0.5);
466+
}
467+
468+
#[test]
469+
fn test_validator_metrics_no_history() {
470+
let aggregator = WeightAggregator::new(EpochConfig::default());
471+
472+
let validator = Keypair::generate().hotkey();
473+
let metrics = aggregator.validator_metrics(&validator, &[]);
474+
475+
assert_eq!(metrics.epochs_participated, 0);
476+
assert_eq!(metrics.epochs_excluded, 0);
477+
assert_eq!(metrics.participation_rate, 0.0);
478+
}
479+
480+
#[test]
481+
fn test_suspicion_reason_variants() {
482+
let reason1 = SuspicionReason::ExcludedFromConsensus;
483+
let reason2 = SuspicionReason::WeightDeviation { deviation: 0.5 };
484+
let reason3 = SuspicionReason::NoParticipation;
485+
486+
// Just verify we can create all variants
487+
assert!(matches!(reason1, SuspicionReason::ExcludedFromConsensus));
488+
assert!(matches!(reason2, SuspicionReason::WeightDeviation { .. }));
489+
assert!(matches!(reason3, SuspicionReason::NoParticipation));
490+
}
491+
492+
#[test]
493+
fn test_emission_with_multiple_weights() {
494+
let aggregator = WeightAggregator::new(EpochConfig::default());
495+
496+
let challenge1 = create_test_challenge("Challenge1", 0.3);
497+
let challenge2 = create_test_challenge("Challenge2", 0.7);
498+
499+
let mut finalized = HashMap::new();
500+
501+
finalized.insert(
502+
challenge1.id,
503+
FinalizedWeights {
504+
challenge_id: challenge1.id,
505+
epoch: 0,
506+
weights: vec![
507+
WeightAssignment::new("agent1".to_string(), 0.8),
508+
WeightAssignment::new("agent2".to_string(), 0.2),
509+
],
510+
participating_validators: vec![],
511+
excluded_validators: vec![],
512+
smoothing_applied: 0.3,
513+
finalized_at: chrono::Utc::now(),
514+
},
515+
);
516+
517+
finalized.insert(
518+
challenge2.id,
519+
FinalizedWeights {
520+
challenge_id: challenge2.id,
521+
epoch: 0,
522+
weights: vec![
523+
WeightAssignment::new("agent3".to_string(), 0.4),
524+
WeightAssignment::new("agent4".to_string(), 0.6),
525+
],
526+
participating_validators: vec![],
527+
excluded_validators: vec![],
528+
smoothing_applied: 0.3,
529+
finalized_at: chrono::Utc::now(),
530+
},
531+
);
532+
533+
let distribution =
534+
aggregator.calculate_emissions(0, 10000, &[challenge1, challenge2], &finalized);
535+
536+
assert_eq!(distribution.epoch, 0);
537+
assert!(!distribution.distributions.is_empty());
538+
539+
// Verify distribution proportions
540+
let total: u64 = distribution.distributions.iter().map(|d| d.emission).sum();
541+
assert!(total <= 10000);
542+
}
543+
544+
#[test]
545+
fn test_empty_finalized_weights() {
546+
let aggregator = WeightAggregator::new(EpochConfig::default());
547+
548+
let challenge = create_test_challenge("Challenge", 0.5);
549+
550+
let mut finalized = HashMap::new();
551+
finalized.insert(
552+
challenge.id,
553+
FinalizedWeights {
554+
challenge_id: challenge.id,
555+
epoch: 0,
556+
weights: vec![], // Empty weights
557+
participating_validators: vec![],
558+
excluded_validators: vec![],
559+
smoothing_applied: 0.0,
560+
finalized_at: chrono::Utc::now(),
561+
},
562+
);
563+
564+
let distribution =
565+
aggregator.calculate_emissions(0, 1000, &[challenge], &finalized);
566+
567+
// Should handle empty weights gracefully
568+
assert_eq!(distribution.epoch, 0);
569+
}
293570
}

0 commit comments

Comments
 (0)