@@ -12,10 +12,12 @@ import (
1212 "google.golang.org/protobuf/types/known/emptypb"
1313 "google.golang.org/protobuf/types/known/timestamppb"
1414
15- "github.com/smartcontractkit/chainlink-common/pkg/logger"
16- "github.com/smartcontractkit/chainlink-common/pkg/workflows/ring/pb"
1715 "github.com/smartcontractkit/libocr/offchainreporting2/types"
1816 "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types"
17+
18+ "github.com/smartcontractkit/chainlink-common/pkg/logger"
19+ "github.com/smartcontractkit/chainlink-common/pkg/workflows/ring/pb"
20+ "github.com/smartcontractkit/chainlink-common/pkg/workflows/shardorchestrator"
1921)
2022
2123type mockArbiter struct {
@@ -443,7 +445,7 @@ func TestPlugin_NoHealthyShardsFallbackToShardZero(t *testing.T) {
443445 })
444446 require .NoError (t , err )
445447
446- transmitter := NewTransmitter (lggr , store , arbiter , "test-account" )
448+ transmitter := NewTransmitter (lggr , store , nil , arbiter , "test-account" )
447449
448450 ctx , cancel := context .WithTimeout (context .Background (), 200 * time .Millisecond )
449451 defer cancel ()
@@ -591,3 +593,158 @@ func TestPlugin_ObservationQuorum(t *testing.T) {
591593 require .True (t , quorum )
592594 })
593595}
596+
597+ func TestPlugin_ShardOrchestratorIntegration (t * testing.T ) {
598+ lggr := logger .Test (t )
599+
600+ // Create both stores
601+ ringStore := NewStore ()
602+ orchestratorStore := shardorchestrator .NewStore (lggr )
603+
604+ // Initialize ring store with healthy shards
605+ ringStore .SetAllShardHealth (map [uint32 ]bool {0 : true , 1 : true , 2 : true })
606+
607+ config := ocr3types.ReportingPluginConfig {
608+ N : 4 , F : 1 ,
609+ }
610+
611+ arbiter := & mockArbiter {}
612+ plugin , err := NewPlugin (ringStore , arbiter , config , lggr , & ConsensusConfig {
613+ BatchSize : 100 ,
614+ TimeToSync : 1 * time .Second ,
615+ })
616+ require .NoError (t , err )
617+
618+ // Create transmitter with both stores
619+ transmitter := NewTransmitter (lggr , ringStore , orchestratorStore , arbiter , "test-account" )
620+
621+ ctx := context .Background ()
622+ now := time .Now ()
623+
624+ t .Run ("initial_workflow_assignments" , func (t * testing.T ) {
625+ // Create observations with workflows
626+ workflows := []string {"wf-A" , "wf-B" , "wf-C" }
627+ aos := makeObservationsWithWantShards (t , []map [uint32 ]* pb.ShardStatus {
628+ {0 : {IsHealthy : true }, 1 : {IsHealthy : true }, 2 : {IsHealthy : true }},
629+ {0 : {IsHealthy : true }, 1 : {IsHealthy : true }, 2 : {IsHealthy : true }},
630+ {0 : {IsHealthy : true }, 1 : {IsHealthy : true }, 2 : {IsHealthy : true }},
631+ }, workflows , now , 3 )
632+
633+ outcomeCtx := ocr3types.OutcomeContext {
634+ SeqNr : 1 ,
635+ PreviousOutcome : nil ,
636+ }
637+
638+ // Generate outcome
639+ outcome , err := plugin .Outcome (ctx , outcomeCtx , nil , aos )
640+ require .NoError (t , err )
641+
642+ // Generate report and transmit
643+ reports , err := plugin .Reports (ctx , 1 , outcome )
644+ require .NoError (t , err )
645+ require .Len (t , reports , 1 )
646+
647+ err = transmitter .Transmit (ctx , types.ConfigDigest {}, 1 , reports [0 ].ReportWithInfo , nil )
648+ require .NoError (t , err )
649+
650+ // Verify ring store was updated
651+ for _ , wf := range workflows {
652+ shard , err := ringStore .GetShardForWorkflow (ctx , wf )
653+ require .NoError (t , err )
654+ require .LessOrEqual (t , shard , uint32 (2 ), "workflow should be assigned to valid shard" )
655+ t .Logf ("Ring store: %s → shard %d" , wf , shard )
656+ }
657+
658+ // Verify orchestrator store was updated with correct state
659+ for _ , wf := range workflows {
660+ mapping , err := orchestratorStore .GetWorkflowMapping (ctx , wf )
661+ require .NoError (t , err )
662+ require .Equal (t , wf , mapping .WorkflowID )
663+ require .LessOrEqual (t , mapping .NewShardID , uint32 (2 ))
664+ require .Equal (t , uint32 (0 ), mapping .OldShardID , "initial assignment should have oldShardID=0" )
665+ require .Equal (t , shardorchestrator .StateSteady , mapping .TransitionState , "initial assignment should be steady" )
666+ t .Logf ("Orchestrator store: %s → shard %d (state: %s)" , wf , mapping .NewShardID , mapping .TransitionState .String ())
667+ }
668+
669+ // Verify version tracking
670+ version := orchestratorStore .GetMappingVersion ()
671+ require .Equal (t , uint64 (1 ), version , "version should increment after first update" )
672+ })
673+
674+ t .Run ("workflow_transition_detected" , func (t * testing.T ) {
675+ // First, establish a baseline with workflows distributed across 3 shards
676+ // Use wantShards=3 to ensure workflows actually get assigned to shard 2
677+ baselineAos := makeObservationsWithWantShards (t , []map [uint32 ]* pb.ShardStatus {
678+ {0 : {IsHealthy : true }, 1 : {IsHealthy : true }, 2 : {IsHealthy : true }},
679+ {0 : {IsHealthy : true }, 1 : {IsHealthy : true }, 2 : {IsHealthy : true }},
680+ {0 : {IsHealthy : true }, 1 : {IsHealthy : true }, 2 : {IsHealthy : true }},
681+ }, []string {"wf-A" , "wf-B" , "wf-C" , "wf-D" , "wf-E" }, now , 3 )
682+
683+ baselineOutcome , err := plugin .Outcome (ctx , ocr3types.OutcomeContext {SeqNr : 2 }, nil , baselineAos )
684+ require .NoError (t , err )
685+
686+ baselineReports , err := plugin .Reports (ctx , 2 , baselineOutcome )
687+ require .NoError (t , err )
688+
689+ err = transmitter .Transmit (ctx , types.ConfigDigest {}, 2 , baselineReports [0 ].ReportWithInfo , nil )
690+ require .NoError (t , err )
691+
692+ // Parse baseline to see which workflows were on shard 2
693+ baselineProto := & pb.Outcome {}
694+ err = proto .Unmarshal (baselineOutcome , baselineProto )
695+ require .NoError (t , err )
696+
697+ workflowsOnShard2 := []string {}
698+ for wfID , route := range baselineProto .Routes {
699+ if route .Shard == 2 {
700+ workflowsOnShard2 = append (workflowsOnShard2 , wfID )
701+ }
702+ t .Logf ("Baseline: %s on shard %d" , wfID , route .Shard )
703+ }
704+ require .NotEmpty (t , workflowsOnShard2 , "at least one workflow should be on shard 2 for this test" )
705+
706+ // Now scale down to 2 shards - workflows on shard 2 MUST move
707+ transitionAos := makeObservationsWithWantShards (t , []map [uint32 ]* pb.ShardStatus {
708+ {0 : {IsHealthy : true }, 1 : {IsHealthy : true }},
709+ {0 : {IsHealthy : true }, 1 : {IsHealthy : true }},
710+ {0 : {IsHealthy : true }, 1 : {IsHealthy : true }},
711+ }, []string {"wf-A" , "wf-B" , "wf-C" , "wf-D" , "wf-E" }, now , 2 )
712+
713+ outcomeCtx := ocr3types.OutcomeContext {
714+ SeqNr : 3 ,
715+ PreviousOutcome : baselineOutcome ,
716+ }
717+
718+ outcome , err := plugin .Outcome (ctx , outcomeCtx , nil , transitionAos )
719+ require .NoError (t , err )
720+
721+ reports , err := plugin .Reports (ctx , 3 , outcome )
722+ require .NoError (t , err )
723+
724+ err = transmitter .Transmit (ctx , types.ConfigDigest {}, 3 , reports [0 ].ReportWithInfo , nil )
725+ require .NoError (t , err )
726+
727+ // Verify orchestrator store shows transition state for workflows that moved from shard 2
728+ outcomeProto := & pb.Outcome {}
729+ err = proto .Unmarshal (outcome , outcomeProto )
730+ require .NoError (t , err )
731+
732+ // Workflows that were on shard 2 must have moved and should show TransitionState
733+ for _ , wfID := range workflowsOnShard2 {
734+ mapping , err := orchestratorStore .GetWorkflowMapping (ctx , wfID )
735+ require .NoError (t , err )
736+
737+ newRoute := outcomeProto .Routes [wfID ]
738+ require .NotEqual (t , uint32 (2 ), newRoute .Shard , "workflow should have moved from shard 2" )
739+ require .Equal (t , shardorchestrator .StateTransitioning , mapping .TransitionState ,
740+ "workflow %s moved from shard 2 to shard %d, should be transitioning" , wfID , newRoute .Shard )
741+ require .Equal (t , uint32 (2 ), mapping .OldShardID , "should track old shard" )
742+ require .Equal (t , newRoute .Shard , mapping .NewShardID , "should track new shard" )
743+ t .Logf ("Workflow %s transitioned: shard 2 → %d" , wfID , newRoute .Shard )
744+ }
745+
746+ // Verify version incremented
747+ version := orchestratorStore .GetMappingVersion ()
748+ require .Equal (t , uint64 (3 ), version , "version should increment after update" )
749+ })
750+ }
0 commit comments