diff --git a/cmd/workflow/simulate/capabilities.go b/cmd/workflow/simulate/capabilities.go index 8da064ae..ffb37c6f 100644 --- a/cmd/workflow/simulate/capabilities.go +++ b/cmd/workflow/simulate/capabilities.go @@ -5,13 +5,13 @@ import ( "github.com/ethereum/go-ethereum/common" + cronservice "github.com/smartcontractkit/capabilities/scheduler/cron/service" corekeys "github.com/smartcontractkit/chainlink-common/keystore/corekeys" "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" capreg "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" confhttpserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp/server" httpserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/http/server" consensusserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/consensus/server" - crontrigger "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/cron/server" httptrigger "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/http/server" "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -21,19 +21,15 @@ import ( // ManualTriggers holds chain-agnostic trigger services used in simulation. type ManualTriggers struct { - ManualCronTrigger *fakes.ManualCronTriggerService + // Capabilities are the capabilities from the capabilities repo. + Capabilities *Capabilities ManualHTTPTrigger *ManualHTTPTriggerService } // NewManualTriggerCapabilities creates and registers cron and HTTP trigger capabilities. // These are chain-agnostic and shared across all chain types. -func NewManualTriggerCapabilities(ctx context.Context, lggr logger.Logger, registry *capreg.Registry, httpTriggerPort int, limits *SimulationLimits) (*ManualTriggers, error) { - manualCronTrigger, err := fakes.NewManualCronTriggerService(lggr) - if err != nil { - return nil, err - } - manualCronTriggerServer := crontrigger.NewCronServer(manualCronTrigger) - if err := registry.Add(ctx, manualCronTriggerServer); err != nil { +func NewManualTriggerCapabilities(ctx context.Context, lggr logger.Logger, registry *capreg.Registry, caps *Capabilities, httpTriggerPort int, limits *SimulationLimits) (*ManualTriggers, error) { + if err := caps.Add(ctx, registry, cronservice.New); err != nil { return nil, err } @@ -49,39 +45,19 @@ func NewManualTriggerCapabilities(ctx context.Context, lggr logger.Logger, regis } return &ManualTriggers{ - ManualCronTrigger: manualCronTrigger, + Capabilities: caps, ManualHTTPTrigger: manualHTTPTrigger, }, nil } -// Start starts cron and HTTP trigger services. +// Start starts the HTTP trigger service. Capabilities from the capabilities repo are started when they are built. func (m *ManualTriggers) Start(ctx context.Context) error { - err := m.ManualCronTrigger.Start(ctx) - if err != nil { - return err - } - - err = m.ManualHTTPTrigger.Start(ctx) - if err != nil { - return err - } - - return nil + return m.ManualHTTPTrigger.Start(ctx) } -// Close closes cron and HTTP trigger services. +// Close closes the HTTP trigger service. Capabilities from the capabilities repo are closed with their services. func (m *ManualTriggers) Close() error { - err := m.ManualCronTrigger.Close() - if err != nil { - return err - } - - err = m.ManualHTTPTrigger.Close() - if err != nil { - return err - } - - return nil + return m.ManualHTTPTrigger.Close() } // NewFakeActionCapabilities builds faked capabilities, then registers them with the capability registry. diff --git a/cmd/workflow/simulate/repo_capabilities.go b/cmd/workflow/simulate/repo_capabilities.go new file mode 100644 index 00000000..e85bda27 --- /dev/null +++ b/cmd/workflow/simulate/repo_capabilities.go @@ -0,0 +1,115 @@ +package simulate + +import ( + "context" + "sync" + + "github.com/jonboulle/clockwork" + "github.com/samber/do/v2" + + "github.com/smartcontractkit/capabilities/libs/capability" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + capreg "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" +) + +// Capabilities builds the simulator's capabilities from the capabilities repo, all from one injector. What the +// simulator injects lets it trigger them early: skipping Clock fires their timers now. +type Capabilities struct { + Clock *SkipClock + // Services are what the capabilities started, for the caller to close, even when Add fails. + Services []services.Service + + lggr logger.Logger + injector *capability.Injector + // selected is the only trigger registration the capabilities are asked for, set before the engine registers. + selected string + triggered chan struct{} +} + +// initCapabilities clones the injector capabilities are embedded from, for the simulator to override what it +// needs to on it. Capabilities provide their own configs, so they run with their defaults unless overridden. +func initCapabilities(lggr logger.Logger) *Capabilities { + return &Capabilities{ + Clock: NewSkipClock(), + lggr: lggr, + injector: capability.EmbedInjector.Clone(), + triggered: make(chan struct{}, 1), + } +} + +// Add builds and starts the capability ctor builds, then registers it. +func (c *Capabilities) Add[T capability.Capability](ctx context.Context, registry *capreg.Registry, ctor func(do.Injector) (T, error)) error { + // StartCapabilities provides the real clock after cloning the injector, so only WithProvider overrides it. + built, svcs, err := capability.StartCapabilities(ctx, c.lggr, c.injector, 1, ctor, + capability.WithProvider(func(i *capability.Injector) error { + return i.ProvideValue[clockwork.Clock](c.Clock) + })) + c.Services = append(c.Services, svcs...) + if err != nil { + return err + } + + return registry.Add(ctx, &selectableTrigger{ExecutableAndTriggerCapability: built[0], capabilities: c}) +} + +// Select makes triggerID the only trigger registration passed on to the capabilities. Call it before the engine +// registers its triggers. +func (c *Capabilities) Select(triggerID string) { + c.selected = triggerID +} + +// WaitForTrigger waits for the selected trigger to send an event. +func (c *Capabilities) WaitForTrigger(ctx context.Context) error { + select { + case <-c.triggered: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// selectableTrigger passes on only the selected trigger registration. Any other is a no-op whose events +// channel closes when it is unregistered. +type selectableTrigger struct { + capabilities.ExecutableAndTriggerCapability + capabilities *Capabilities + unselected sync.Map // trigger ID to chan capabilities.TriggerResponse +} + +func (s *selectableTrigger) RegisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) { + if request.TriggerID != s.capabilities.selected { + events := make(chan capabilities.TriggerResponse) + s.unselected.Store(request.TriggerID, events) + return events, nil + } + + events, err := s.ExecutableAndTriggerCapability.RegisterTrigger(ctx, request) + if err != nil { + return nil, err + } + + forwarded := make(chan capabilities.TriggerResponse) + go func() { + defer close(forwarded) + for event := range events { + forwarded <- event + select { + case s.capabilities.triggered <- struct{}{}: + default: + } + } + }() + + return forwarded, nil +} + +func (s *selectableTrigger) UnregisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) error { + if events, ok := s.unselected.LoadAndDelete(request.TriggerID); ok { + close(events.(chan capabilities.TriggerResponse)) + return nil + } + + return s.ExecutableAndTriggerCapability.UnregisterTrigger(ctx, request) +} diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 21d623ee..dcfc6636 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -121,7 +121,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { simulateCmd.Flags().Int("trigger-index", -1, "Index of the trigger to run (0-based)") simulateCmd.Flags().String("http-payload", "", "HTTP trigger payload as JSON string or path to JSON file") simulateCmd.Flags().Int("http-trigger-port", defaultHTTPTriggerServerPort, "Port used by the local HTTP trigger server") - simulateCmd.Flags().Bool("listen", false, "Listen for HTTP requests or supported log triggers and run the simulator for each match (not supported by cron)") + simulateCmd.Flags().Bool("listen", false, "Listen for HTTP requests, cron schedules or supported log triggers and run the simulator for each match") // Register chain-type-specific CLI flags (e.g., --evm-tx-hash). chain.RegisterAllCLIFlags(simulateCmd) @@ -529,15 +529,18 @@ func run( beholderStarted = true } + caps := initCapabilities(lggr.Named("Capabilities")) + // Register chain-agnostic cron and HTTP triggers triggerLggr := lggr.Named("TriggerCapabilities") var err error - manualTriggerCaps, err = NewManualTriggerCapabilities(ctx, triggerLggr, registry, inputs.HTTPTriggerPort, simLimits) + manualTriggerCaps, err = NewManualTriggerCapabilities(ctx, triggerLggr, registry, caps, inputs.HTTPTriggerPort, simLimits) + srvcs = append(srvcs, caps.Services...) if err != nil { setLifecycleErr(fmt.Errorf("failed to create trigger capabilities: %w", err)) return registry, srvcs } - srvcs = append(srvcs, manualTriggerCaps.ManualCronTrigger, manualTriggerCaps.ManualHTTPTrigger) + srvcs = append(srvcs, manualTriggerCaps.ManualHTTPTrigger) // nil capLimits disables enforcement. var capLimits *cresettings.Workflows @@ -1044,24 +1047,27 @@ func makeBeforeStartInteractive(holder *TriggerInfoAndBeforeStart, inputs Inputs triggerRegistrationID := fmt.Sprintf("trigger_reg_1111111111111111111111111111111111111111111111111111111111111111_%d", triggerIndex) trigger := holder.TriggerToRun.Id manualTriggerCaps := manualTriggerCapsGetter() + manualTriggerCaps.Capabilities.Select(triggerRegistrationID) switch trigger { case "cron-trigger@1.0.0": + caps := manualTriggerCaps.Capabilities + holder.ListenSupported = true holder.TriggerFunc = func() error { - skipWaitSignal := make(chan struct{}, 1) - userPromptCtx, cancel := context.WithCancel(ctx) defer cancel() - go func() { - ui.Line() - pressed := ui.WaitForEnter(userPromptCtx, "Cron scheduler started. Press Enter to skip waiting...") - if pressed { - skipWaitSignal <- struct{}{} - } - }() + // Listening runs on the schedule, so only a single run can skip ahead to it. + if !inputs.Listen { + go func() { + ui.Line() + if ui.WaitForEnter(userPromptCtx, "Cron scheduler started. Press Enter to skip waiting...") { + caps.Clock.Skip() + } + }() + } - return manualTriggerCaps.ManualCronTrigger.ManualTrigger(ctx, triggerRegistrationID, skipWaitSignal) + return caps.WaitForTrigger(ctx) } case "http-trigger@1.0.0-alpha": payload, err := getHTTPTriggerPayloadFromInput(inputs.InvocationDir, inputs.HTTPPayload) @@ -1187,14 +1193,19 @@ func makeBeforeStartNonInteractive(holder *TriggerInfoAndBeforeStart, inputs Inp triggerRegistrationID := fmt.Sprintf("trigger_reg_1111111111111111111111111111111111111111111111111111111111111111_%d", inputs.TriggerIndex) trigger := holder.TriggerToRun.Id manualTriggerCaps := manualTriggerCapsGetter() + manualTriggerCaps.Capabilities.Select(triggerRegistrationID) switch trigger { case "cron-trigger@1.0.0": + caps := manualTriggerCaps.Capabilities + holder.ListenSupported = true holder.TriggerFunc = func() error { - skipWaitSignal := make(chan struct{}, 1) - // With cron schedule on non-interactive mode - skipWaitSignal <- struct{}{} - return manualTriggerCaps.ManualCronTrigger.ManualTrigger(ctx, triggerRegistrationID, skipWaitSignal) + // A single non-interactive run does not wait for the schedule. Listening runs on it. + if !inputs.Listen { + caps.Clock.Skip() + } + + return caps.WaitForTrigger(ctx) } 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 7dac1193..0fc3887a 100644 --- a/cmd/workflow/simulate/simulate_test.go +++ b/cmd/workflow/simulate/simulate_test.go @@ -19,12 +19,15 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/anypb" + cronprotos "github.com/smartcontractkit/capabilities/scheduler/cron/protos" + cronservice "github.com/smartcontractkit/capabilities/scheduler/cron/service" commoncaps "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - crontypedapi "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/cron" + capreg "github.com/smartcontractkit/chainlink-common/pkg/capabilities/registry" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" 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" @@ -505,44 +508,50 @@ func TestSimulateResolveInputs_InvocationDir(t *testing.T) { // 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. +// completes immediately without waiting for the actual cron schedule, and only +// the selected trigger runs. 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() }) + caps := initCapabilities(logger.Test(t)) + registry := capreg.NewRegistry(logger.Test(t)) + err := caps.Add(ctx, registry, cronservice.New) + t.Cleanup(func() { require.NoError(t, services.MultiCloser(caps.Services).Close()) }) + require.NoError(t, err) - // 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, HTTPTriggerPort: defaultHTTPTriggerServerPort} - manualTriggers := &ManualTriggers{ManualCronTrigger: cronSvc} beforeStart := makeBeforeStartNonInteractive(holder, inputs, func() *ManualTriggers { - return manualTriggers + return &ManualTriggers{Capabilities: caps} }, func(err error) { require.NoError(t, err, "unexpected BeforeStart error") }, nil) - triggerSub := []*pb.TriggerSubscription{{Id: "cron-trigger@1.0.0"}} - beforeStart(ctx, simulator.RunnerConfig{}, nil, nil, triggerSub) + payload, err := anypb.New(&cronprotos.Config{Schedule: "0 0 * * *"}) + require.NoError(t, err) + sub := &pb.TriggerSubscription{Id: cronprotos.CronID, Method: "Trigger", Payload: payload} + beforeStart(ctx, simulator.RunnerConfig{}, registry, nil, []*pb.TriggerSubscription{sub, sub}) require.NotNil(t, holder.TriggerFunc) + // Register both triggers as the engine does, after BeforeStart, with the IDs makeBeforeStartNonInteractive uses. + trigger, err := registry.GetTrigger(ctx, cronprotos.CronID) + require.NoError(t, err) + register := func(index int) <-chan commoncaps.TriggerResponse { + events, err := trigger.RegisterTrigger(ctx, commoncaps.TriggerRegistrationRequest{ + TriggerID: fmt.Sprintf("trigger_reg_1111111111111111111111111111111111111111111111111111111111111111_%d", index), + Metadata: commoncaps.RequestMetadata{WorkflowID: "test-workflow"}, + Method: sub.Method, + Payload: sub.Payload, + }) + require.NoError(t, err) + return events + } + selected := register(triggerIndex) + unselected := register(triggerIndex + 1) + done := make(chan error, 1) go func() { done <- holder.TriggerFunc() }() @@ -550,7 +559,19 @@ func TestNonInteractiveCronTriggerDoesNotBlockOnSchedule(t *testing.T) { 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") + t.Fatal("TriggerFunc blocked waiting for cron schedule") + } + + select { + case <-selected: + case <-time.After(3 * time.Second): + t.Fatal("the selected trigger sent no event") + } + + select { + case e := <-unselected: + t.Fatalf("unexpected event %s from a trigger that was not selected", e.Event.ID) + case <-time.After(100 * time.Millisecond): } } diff --git a/cmd/workflow/simulate/skip_clock.go b/cmd/workflow/simulate/skip_clock.go new file mode 100644 index 00000000..42182e2c --- /dev/null +++ b/cmd/workflow/simulate/skip_clock.go @@ -0,0 +1,79 @@ +package simulate + +import ( + "sync" + "time" + + "github.com/jonboulle/clockwork" +) + +// SkipClock is the real clock, except that Skip fires the functions waiting on its AfterFunc timers now. +type SkipClock struct { + clockwork.Clock + + mu sync.Mutex + pending map[*skipTimer]bool +} + +func NewSkipClock() *SkipClock { + return &SkipClock{Clock: clockwork.NewRealClock(), pending: map[*skipTimer]bool{}} +} + +func (c *SkipClock) AfterFunc(d time.Duration, f func()) clockwork.Timer { + t := &skipTimer{clock: c, f: f} + c.mu.Lock() + defer c.mu.Unlock() + c.pending[t] = true + t.Timer = c.Clock.AfterFunc(d, t.fire) + return t +} + +// Skip fires every pending AfterFunc timer now, instead of when it is due. +func (c *SkipClock) Skip() { + c.mu.Lock() + timers := make([]*skipTimer, 0, len(c.pending)) + for t := range c.pending { + t.Timer.Stop() + timers = append(timers, t) + } + clear(c.pending) + c.mu.Unlock() + + for _, t := range timers { + go t.f() + } +} + +// take removes t from the pending timers, reporting whether it was pending. +func (c *SkipClock) take(t *skipTimer) bool { + c.mu.Lock() + defer c.mu.Unlock() + pending := c.pending[t] + delete(c.pending, t) + return pending +} + +type skipTimer struct { + clockwork.Timer + clock *SkipClock + f func() +} + +// fire runs f when the timer is due, unless Skip already ran it. +func (t *skipTimer) fire() { + if t.clock.take(t) { + t.f() + } +} + +func (t *skipTimer) Stop() bool { + t.clock.take(t) + return t.Timer.Stop() +} + +func (t *skipTimer) Reset(d time.Duration) bool { + t.clock.mu.Lock() + defer t.clock.mu.Unlock() + t.clock.pending[t] = true + return t.Timer.Reset(d) +} diff --git a/cmd/workflow/simulate/skip_clock_test.go b/cmd/workflow/simulate/skip_clock_test.go new file mode 100644 index 00000000..229a2f8c --- /dev/null +++ b/cmd/workflow/simulate/skip_clock_test.go @@ -0,0 +1,52 @@ +package simulate + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSkipClock_SkipFiresPendingTimersOnce(t *testing.T) { + t.Parallel() + + clock := NewSkipClock() + var fired atomic.Int32 + clock.AfterFunc(200*time.Millisecond, func() { fired.Add(1) }) + + clock.Skip() + require.Eventually(t, func() bool { return fired.Load() == 1 }, time.Second, 10*time.Millisecond) + + // The real timer was stopped, so it does not fire again when due. + time.Sleep(400 * time.Millisecond) + assert.Equal(t, int32(1), fired.Load()) +} + +func TestSkipClock_TimersFireWhenDue(t *testing.T) { + t.Parallel() + + clock := NewSkipClock() + var fired atomic.Int32 + clock.AfterFunc(10*time.Millisecond, func() { fired.Add(1) }) + require.Eventually(t, func() bool { return fired.Load() == 1 }, time.Second, 10*time.Millisecond) + + // A fired timer is no longer pending, so skipping does not run it again. + clock.Skip() + time.Sleep(50 * time.Millisecond) + assert.Equal(t, int32(1), fired.Load()) +} + +func TestSkipClock_StoppedTimersDoNotFire(t *testing.T) { + t.Parallel() + + clock := NewSkipClock() + var fired atomic.Int32 + timer := clock.AfterFunc(time.Hour, func() { fired.Add(1) }) + timer.Stop() + + clock.Skip() + time.Sleep(50 * time.Millisecond) + assert.Equal(t, int32(0), fired.Load()) +} diff --git a/go.mod b/go.mod index 6866e5be..8b113d47 100644 --- a/go.mod +++ b/go.mod @@ -25,14 +25,19 @@ require ( github.com/jarcoal/httpmock v1.4.1 github.com/jedib0t/go-pretty/v6 v6.6.5 github.com/joho/godotenv v1.5.1 + github.com/jonboulle/clockwork v0.5.0 github.com/machinebox/graphql v0.2.2 github.com/pkg/errors v0.9.1 + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rs/zerolog v1.35.1 + github.com/samber/do/v2 v2.1.0 + github.com/smartcontractkit/capabilities/libs v0.0.0-20261029172142-a56714b80bb4 + github.com/smartcontractkit/capabilities/scheduler/cron v0.0.0-20261029172142-a56714b80bb4 github.com/smartcontractkit/chain-selectors v1.0.111 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260930142354-8fdc7816bef3 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20261022160347-6812ecc7b2db github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260713161920-de075095648b - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260916235422-d73d783612ca + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20261013212015-e622e8b83b6b github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260821155228-fa1d775e2138 github.com/smartcontractkit/chainlink-solana v1.3.1-0.20260824161511-a7c0a2bf25a0 github.com/smartcontractkit/chainlink-solana/contracts v0.0.0-20260824161511-a7c0a2bf25a0 @@ -245,6 +250,7 @@ require ( github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect + github.com/huandu/go-clone v1.7.3 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -260,7 +266,6 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect - github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.19.1 // indirect @@ -315,13 +320,13 @@ require ( github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/cors v1.11.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect + github.com/samber/go-type-to-string v1.8.0 // indirect github.com/samber/lo v1.53.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect @@ -337,7 +342,8 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260916222901-720a003dab50 // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260916222901-720a003dab50 // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260916222901-720a003dab50 // indirect - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 // indirect + github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20261005132539-b7d63d5fd549 // indirect + github.com/smartcontractkit/chainlink-common/x/config v0.0.0-20261014172408-e5560b3eeb33 // indirect github.com/smartcontractkit/chainlink-data-streams v1.1.2-0.20261002081259-6c2163b21db9 // indirect github.com/smartcontractkit/chainlink-deployments-framework v0.122.2 // indirect github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260930175125-de5769a7a922 // indirect diff --git a/go.sum b/go.sum index 03779f12..827feb76 100644 --- a/go.sum +++ b/go.sum @@ -714,6 +714,9 @@ github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iU github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= +github.com/huandu/go-clone v1.7.3 h1:rtQODA+ABThEn6J5LBTppJfKmZy/FwfpMUWa8d01TTQ= +github.com/huandu/go-clone v1.7.3/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= github.com/huandu/skiplist v1.2.0 h1:gox56QD77HzSC0w+Ws3MH3iie755GBJU1OER3h5VsYw= github.com/huandu/skiplist v1.2.0/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= @@ -1117,6 +1120,10 @@ github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDc github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/samber/do/v2 v2.1.0 h1:lqCHn05XvY3VqwxvZDQPSkH+jIGWSVHUrSVLEbPOopo= +github.com/samber/do/v2 v2.1.0/go.mod h1:wJBoiaZcUZyGuraOhfz15b517ZMogGs+U03DvnqvT6Q= +github.com/samber/go-type-to-string v1.8.0 h1:5z6tDTjtXxkIAoAuHAZYMYR8mkBZjVgeSH7jcSLqc8w= +github.com/samber/go-type-to-string v1.8.0/go.mod h1:jpU77vIDoIxkahknKDoEx9C8bQ1ADnh2sotZ8I4QqBU= github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= @@ -1153,6 +1160,14 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/smartcontractkit/capabilities/libs v0.0.0-20261027153859-c97960a9ddb6 h1:VNkXQWev0gtB5GF6pdoIpZIcghDmy0Wa8nRXOKh+3BA= +github.com/smartcontractkit/capabilities/libs v0.0.0-20261027153859-c97960a9ddb6/go.mod h1:C35VbmSWrQUOm9L93id700ksrhsvAUQI+Lg0siiH6PY= +github.com/smartcontractkit/capabilities/libs v0.0.0-20261029172142-a56714b80bb4 h1:g0GAgl8TNncd4r+vGjOaRAieRMKeQO41Awj9WvAQ4gI= +github.com/smartcontractkit/capabilities/libs v0.0.0-20261029172142-a56714b80bb4/go.mod h1:C35VbmSWrQUOm9L93id700ksrhsvAUQI+Lg0siiH6PY= +github.com/smartcontractkit/capabilities/scheduler/cron v0.0.0-20261027153859-c97960a9ddb6 h1:fdxORuDPKm/PmlVq5JsatUtLstNnMB9lwsm78jszbEw= +github.com/smartcontractkit/capabilities/scheduler/cron v0.0.0-20261027153859-c97960a9ddb6/go.mod h1:msKbXRIm5lRfRB9HuY2YoxlTHoXUh7uMuh8mGKFqFlg= +github.com/smartcontractkit/capabilities/scheduler/cron v0.0.0-20261029172142-a56714b80bb4 h1:6fhzQTMBtVk14K34VCjdyH/ZJF1q91fUqKwwMtEf+QE= +github.com/smartcontractkit/capabilities/scheduler/cron v0.0.0-20261029172142-a56714b80bb4/go.mod h1:msKbXRIm5lRfRB9HuY2YoxlTHoXUh7uMuh8mGKFqFlg= github.com/smartcontractkit/ccip-owner-contracts v0.2.1 h1:HgrVtzpyqrPCQiyIeoUKcPK/S+TQI76F3CNU6/hH+6s= github.com/smartcontractkit/ccip-owner-contracts v0.2.1/go.mod h1:dpKtlTVEKohsbhnPe/r1oLFqfq7TP7iKvMaUOiidAaM= github.com/smartcontractkit/chain-selectors v1.0.111 h1:urb/qOyRanxDn+Wras+LULHEHSlJ4tYnfeAHzeMBn5U= @@ -1175,10 +1190,16 @@ github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0 github.com/smartcontractkit/chainlink-ccv v0.13.1-0.20260918171034-c93b0d2ef3c0/go.mod h1:gX7FJzSnGQx4RAlsn2rRW8IQBlEFZYiXFXEEB5jnQgA= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260930142354-8fdc7816bef3 h1:ihs6hN8KnaszTFYZvtUxs03FKB/IxBg4bqJLzdDtWYw= github.com/smartcontractkit/chainlink-common v0.11.2-0.20260930142354-8fdc7816bef3/go.mod h1:2AvwgNr3I6s0Ny1qmovy/stHQIihEv/9g74aNYzQYYo= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20261022160347-6812ecc7b2db h1:+XNlv+GFy2d5HQ7oRzAY5GrjTKaeiY88+OQLrd7+mts= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20261022160347-6812ecc7b2db/go.mod h1:ZRHl1hjJyTPrnv669e3thza3U7Z2as5zKCDZP0kJXuw= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d h1:M9xuiIvdiHtim4nlxCVajyC/Mk63tAg9ESWb/biAhNA= github.com/smartcontractkit/chainlink-common/keystore v1.3.1-0.20260903141829-ef07b52a737d/go.mod h1:rI2njmTlyTvh+xhmORmK6qX3+V1ycDAqGJLu8LS8WpU= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92 h1:Z1OIGukDhJD+Gknr5G07Vtml24E7BwKaIQoWzV/SdqA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260915184316-2730f1867c92/go.mod h1:Scyk7O80fde5rJF3Ty3fEE3xiS6j84eWbu8/2gl3pIY= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20261005132539-b7d63d5fd549 h1:J4vYRJxkBO8Yf//17wScKjYWmS45VZaFWx0zt+4ETEU= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20261005132539-b7d63d5fd549/go.mod h1:Scyk7O80fde5rJF3Ty3fEE3xiS6j84eWbu8/2gl3pIY= +github.com/smartcontractkit/chainlink-common/x/config v0.0.0-20261014172408-e5560b3eeb33 h1:9aS43MBP2grzsxpppcqosVkaeKFEfqfcIG2puIec3y4= +github.com/smartcontractkit/chainlink-common/x/config v0.0.0-20261014172408-e5560b3eeb33/go.mod h1:BseXIPMhfBpV2z/S+tzEFI10LbAzr7HfRIEnPQE1QQY= github.com/smartcontractkit/chainlink-data-streams v1.1.2-0.20261002081259-6c2163b21db9 h1:C7Dv8SRUU97tsUJvDDOmntJI6RZxQflIi88NlvdUnCQ= github.com/smartcontractkit/chainlink-data-streams v1.1.2-0.20261002081259-6c2163b21db9/go.mod h1:ExmEs17bPBrsjV8XUs+Om5vvT/twPHeYfmAMwEnrtQQ= github.com/smartcontractkit/chainlink-deployments-framework v0.122.2 h1:UtkIAbqWYtbExgCBEhQCJMEZpSrcAgPnk8GvFOGXuxA= @@ -1211,6 +1232,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251 github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260916235422-d73d783612ca h1:lNmb9YC3RDqPi9nMnA5BG5ok4Sik/EPLgXpssZl3oz4= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260916235422-d73d783612ca/go.mod h1:/i8hjTPFdVWHiY+QjeSiVS2Z3GB3WAZznGgXHstC02E= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20261013212015-e622e8b83b6b h1:tlxPkVQ6ZpJIElqUWc4lU5VH4BB6OzhckG/gHwpaaa0= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20261013212015-e622e8b83b6b/go.mod h1:/i8hjTPFdVWHiY+QjeSiVS2Z3GB3WAZznGgXHstC02E= github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 h1:SG+wAsNyAcA6Kk19ljuxi3HK9Ll2lpHik8OKoY4x7A0= github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY= github.com/smartcontractkit/chainlink-protos/job-distributor v0.20.1-0.20260701185448-696c075849ea h1:gdA7tZgcRYMq3ryi7uNopYgn6s26g04Haw9SdmLRHzE= diff --git a/test/test_project/blank_workflow/config.json b/test/test_project/blank_workflow/config.json index c545f2e6..c00ba0b6 100644 --- a/test/test_project/blank_workflow/config.json +++ b/test/test_project/blank_workflow/config.json @@ -1,5 +1,5 @@ { - "schedule": "*/3 * * * * *", + "schedule": "*/30 * * * * *", "url": "https://api.real-time-reserves.verinumus.io", "evms": [ {