feat(automations): 自动化引擎(cron 调度 + 事件触发 + AI 管线动作 + 提醒调度,bus automation.result/reminder.trigger)
This commit is contained in:
1 parent
a3f6f4f687
commit
ff34b62079
10 files changed
+1818
No files matched your search
@@ -0,0 +1,165 @@
|
||||
package automations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
||||
)
|
||||
|
||||
func TestValidateCron(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, err := New(Config{}, Deps{Bus: bus.New()})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
expr string
|
||||
cfg Config
|
||||
want bool
|
||||
}{
|
||||
{name: "standard 5-field", expr: "*/5 * * * *", want: true},
|
||||
{name: "standard 5-field with names", expr: "0 9 * * MON", want: true},
|
||||
{name: "descriptor every", expr: "@every 1h30m", want: true},
|
||||
{name: "descriptor midnight", expr: "@midnight", want: true},
|
||||
{name: "6-field rejected by default", expr: "* * * * * *", want: false},
|
||||
{name: "6-field accepted with seconds", expr: "*/10 * * * * *", cfg: Config{WithSeconds: true}, want: true},
|
||||
{name: "garbage rejected", expr: "not a cron", want: false},
|
||||
{name: "empty rejected", expr: "", want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ss := s
|
||||
if tt.cfg.WithSeconds {
|
||||
ss, err = New(tt.cfg, Deps{Bus: bus.New()})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
}
|
||||
err := ss.ValidateCron(tt.expr)
|
||||
if got := err == nil; got != tt.want {
|
||||
t.Fatalf("ValidateCron(%q) err = %v, wantValid=%v", tt.expr, err, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddSchedule_InvalidCronRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, err := New(Config{}, Deps{Bus: bus.New()})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
act, err := NewFuncAction("x", func(context.Context, any) (any, error) { return nil, nil })
|
||||
if err != nil {
|
||||
t.Fatalf("NewFuncAction: %v", err)
|
||||
}
|
||||
if _, err := s.AddSchedule(context.Background(), "not a cron", act); !errors.Is(err, ErrInvalidCron) {
|
||||
t.Fatalf("err = %v, want ErrInvalidCron", err)
|
||||
}
|
||||
if _, err := s.AddSchedule(context.Background(), "* * * * * *", act); !errors.Is(err, ErrInvalidCron) {
|
||||
t.Fatalf("6-field without seconds: err = %v, want ErrInvalidCron", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduler_RejectsNilAction(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, err := New(Config{}, Deps{Bus: bus.New()})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
if _, err := s.AddSchedule(context.Background(), "*/5 * * * *", nil); !errors.Is(err, ErrNilAction) {
|
||||
t.Fatalf("err = %v, want ErrNilAction", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduler_StartStopIdempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, err := New(Config{}, Deps{Bus: bus.New()})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if err := s.Start(ctx); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
if err := s.Start(ctx); err != nil {
|
||||
t.Fatalf("second Start: %v", err)
|
||||
}
|
||||
if err := s.Stop(ctx); err != nil {
|
||||
t.Fatalf("Stop: %v", err)
|
||||
}
|
||||
if err := s.Stop(ctx); err != nil {
|
||||
t.Fatalf("second Stop: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduler_FiresOnDue(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := bus.New()
|
||||
defer b.Close()
|
||||
|
||||
got := make(chan bus.Event, 1)
|
||||
unsub, err := b.Subscribe(context.Background(), SubjectResult, func(_ context.Context, ev bus.Event) error {
|
||||
got <- ev
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("subscribe: %v", err)
|
||||
}
|
||||
defer unsub()
|
||||
|
||||
s, err := New(Config{}, Deps{Bus: b})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
act, err := NewFuncAction("tick", func(context.Context, any) (any, error) { return "ok", nil })
|
||||
if err != nil {
|
||||
t.Fatalf("NewFuncAction: %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
if _, err := s.AddSchedule(ctx, "@every 50ms", act); err != nil {
|
||||
t.Fatalf("AddSchedule: %v", err)
|
||||
}
|
||||
if err := s.Start(ctx); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case ev := <-got:
|
||||
res, ok := ev.Payload.(Result)
|
||||
if !ok {
|
||||
t.Fatalf("payload type = %T, want Result", ev.Payload)
|
||||
}
|
||||
if res.Kind != "schedule" || res.Action != "tick" || res.Output != "ok" {
|
||||
t.Fatalf("result = %+v, want {schedule tick ok}", res)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("schedule did not fire")
|
||||
}
|
||||
if err := s.Stop(ctx); err != nil {
|
||||
t.Fatalf("Stop: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduler_WithSecondsAcceptsSixField(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, err := New(Config{WithSeconds: true}, Deps{Bus: bus.New()})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
act, err := NewFuncAction("tick", func(context.Context, any) (any, error) { return nil, nil })
|
||||
if err != nil {
|
||||
t.Fatalf("NewFuncAction: %v", err)
|
||||
}
|
||||
if _, err := s.AddSchedule(context.Background(), "*/1 * * * * *", act); err != nil {
|
||||
t.Fatalf("6-field with seconds rejected: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user