diff --git a/pkg/tasks/check_consensus_reorgs/task.go b/pkg/tasks/check_consensus_reorgs/task.go index 6a17899b..8b06c7f6 100644 --- a/pkg/tasks/check_consensus_reorgs/task.go +++ b/pkg/tasks/check_consensus_reorgs/task.go @@ -159,6 +159,11 @@ func (t *Task) runCheck() types.TaskResult { epochCount := currentEpoch.Number() - t.startEpoch + return t.evaluateReorgs(epochCount) +} + +// evaluateReorgs applies the configured reorg thresholds to the observed counts. +func (t *Task) evaluateReorgs(epochCount uint64) types.TaskResult { if t.config.MinCheckEpochCount > 0 && epochCount < t.config.MinCheckEpochCount { t.logger.Warnf("Check missed: checked %v epochs, but need >= %v", epochCount, t.config.MinCheckEpochCount) return types.TaskResultNone @@ -169,6 +174,11 @@ func (t *Task) runCheck() types.TaskResult { return types.TaskResultFailure } + if t.config.MaxTotalReorgs > 0 && t.totalReorgs > t.config.MaxTotalReorgs { + t.logger.Warnf("check failed: max total reorgs exceeded. (have: %v, want <= %v)", t.totalReorgs, t.config.MaxTotalReorgs) + return types.TaskResultFailure + } + return types.TaskResultSuccess } diff --git a/pkg/tasks/check_consensus_reorgs/task_test.go b/pkg/tasks/check_consensus_reorgs/task_test.go new file mode 100644 index 00000000..3f413aa4 --- /dev/null +++ b/pkg/tasks/check_consensus_reorgs/task_test.go @@ -0,0 +1,63 @@ +package checkconsensusreorgs + +import ( + "io" + "testing" + + "github.com/ethpandaops/assertoor/pkg/types" + "github.com/sirupsen/logrus" +) + +func newEvalTask(cfg Config, totalReorgs uint64) *Task { + log := logrus.New() + log.SetOutput(io.Discard) + + return &Task{ + config: cfg, + logger: log, + totalReorgs: totalReorgs, + } +} + +// TestEvaluateReorgsMaxTotal covers the maxTotalReorgs bound, which was declared and +// documented but never enforced, so a run exceeding it still passed. +func TestEvaluateReorgsMaxTotal(t *testing.T) { + tests := []struct { + name string + maxTotal uint64 + totalReorgs uint64 + epochCount uint64 + want types.TaskResult + }{ + {"under the bound passes", 5, 3, 10, types.TaskResultSuccess}, + {"at the bound passes", 5, 5, 10, types.TaskResultSuccess}, + {"over the bound fails", 5, 6, 10, types.TaskResultFailure}, + {"unset bound is ignored", 0, 1000, 10, types.TaskResultSuccess}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + task := newEvalTask(Config{MaxTotalReorgs: tc.maxTotal}, tc.totalReorgs) + + if got := task.evaluateReorgs(tc.epochCount); got != tc.want { + t.Fatalf("maxTotalReorgs=%d totalReorgs=%d: got %v, want %v", tc.maxTotal, tc.totalReorgs, got, tc.want) + } + }) + } +} + +// TestEvaluateReorgsOtherBounds guards the neighbouring thresholds so the added +// check does not shadow them. +func TestEvaluateReorgsOtherBounds(t *testing.T) { + // minCheckEpochCount not yet reached -> inconclusive + task := newEvalTask(Config{MinCheckEpochCount: 5}, 0) + if got := task.evaluateReorgs(2); got != types.TaskResultNone { + t.Fatalf("min epoch not reached: got %v, want None", got) + } + + // maxReorgsPerEpoch exceeded -> failure + task = newEvalTask(Config{MaxReorgsPerEpoch: 1.0}, 30) + if got := task.evaluateReorgs(10); got != types.TaskResultFailure { + t.Fatalf("max reorgs per epoch exceeded: got %v, want Failure", got) + } +}