@@ -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