From f6d232e7ffb53ab6ae07bc206dcdab32a5f73710 Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Tue, 19 May 2026 16:55:20 -0300 Subject: [PATCH] Fix non-interactive cron trigger blocking in simulation --- cmd/workflow/simulate/simulate.go | 5 +-- cmd/workflow/simulate/simulate_test.go | 60 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 9f374991..7d0ca445 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -807,12 +807,9 @@ func makeBeforeStartNonInteractive(holder *TriggerInfoAndBeforeStart, inputs Inp case "cron-trigger@1.0.0": holder.TriggerFunc = func() error { skipWaitSignal := make(chan struct{}, 1) - if err := manualTriggerCaps.ManualCronTrigger.ManualTrigger(ctx, triggerRegistrationID, skipWaitSignal); err != nil { - return err - } // With cron schedule on non-interactive mode skipWaitSignal <- struct{}{} - return nil + return manualTriggerCaps.ManualCronTrigger.ManualTrigger(ctx, triggerRegistrationID, skipWaitSignal) } case "http-trigger@1.0.0-alpha": if strings.TrimSpace(inputs.HTTPPayload) == "" { diff --git a/cmd/workflow/simulate/simulate_test.go b/cmd/workflow/simulate/simulate_test.go index 1d24423a..847d7ae2 100644 --- a/cmd/workflow/simulate/simulate_test.go +++ b/cmd/workflow/simulate/simulate_test.go @@ -1,6 +1,7 @@ package simulate import ( + "context" "encoding/base64" "fmt" "io" @@ -8,11 +9,19 @@ import ( "path/filepath" rt "runtime" "testing" + "time" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + commoncaps "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + crontypedapi "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/cron" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + pb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/fakes" + simulator "github.com/smartcontractkit/chainlink/v2/core/services/workflows/cmd/cre/utils" + cmdcommon "github.com/smartcontractkit/cre-cli/cmd/common" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/settings" @@ -438,6 +447,57 @@ func TestSimulateResolveInputs_InvocationDir(t *testing.T) { assert.Equal(t, invocationDir, inputs.InvocationDir) } +// TestNonInteractiveCronTriggerDoesNotBlockOnSchedule verifies that when the +// simulator runs in non-interactive mode with a cron trigger, TriggerFunc +// completes immediately without waiting for the actual cron schedule. +// +// The previous broken implementation sent skipWaitSignal *after* ManualTrigger +// returned, so ManualTrigger blocked in its select until the real cron job fired +// (up to 60 s). The fix pre-fills the channel before calling ManualTrigger. +func TestNonInteractiveCronTriggerDoesNotBlockOnSchedule(t *testing.T) { + t.Parallel() + + cronSvc, err := fakes.NewManualCronTriggerService(logger.Test(t)) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + require.NoError(t, cronSvc.Start(ctx)) + t.Cleanup(func() { _ = cronSvc.Close() }) + + // Register the trigger with the ID that makeBeforeStartNonInteractive will use. + triggerIndex := 0 + triggerRegistrationID := fmt.Sprintf("trigger_reg_1111111111111111111111111111111111111111111111111111111111111111_%d", triggerIndex) + _, capErr := cronSvc.RegisterTrigger(ctx, triggerRegistrationID, + commoncaps.RequestMetadata{WorkflowID: "test-workflow"}, + &crontypedapi.Config{Schedule: "* * * * *"}, + ) + require.Nil(t, capErr) + + holder := &TriggerInfoAndBeforeStart{} + inputs := Inputs{TriggerIndex: triggerIndex} + manualTriggers := &ManualTriggers{ManualCronTrigger: cronSvc} + + beforeStart := makeBeforeStartNonInteractive(holder, inputs, func() *ManualTriggers { + return manualTriggers + }) + + triggerSub := []*pb.TriggerSubscription{{Id: "cron-trigger@1.0.0"}} + beforeStart(ctx, simulator.RunnerConfig{}, nil, nil, triggerSub) + require.NotNil(t, holder.TriggerFunc) + + done := make(chan error, 1) + go func() { done <- holder.TriggerFunc() }() + + select { + case err := <-done: + require.NoError(t, err) + case <-time.After(3 * time.Second): + t.Fatal("TriggerFunc blocked waiting for cron schedule; skipWaitSignal must be sent before ManualTrigger is called") + } +} + func TestSimulateConfigFlagsMutuallyExclusive(t *testing.T) { t.Parallel()