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,168 @@
|
||||
package automations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
||||
)
|
||||
|
||||
func TestReminder_AddListCancel(t *testing.T) {
|
||||
t.Parallel()
|
||||
rs, err := NewReminder(Config{}, Deps{Bus: bus.New()})
|
||||
if err != nil {
|
||||
t.Fatalf("NewReminder: %v", err)
|
||||
}
|
||||
base := time.Now().Add(time.Hour)
|
||||
r1 := Reminder{ID: "later", At: base.Add(time.Hour), Message: "b"}
|
||||
r2 := Reminder{ID: "sooner", At: base, Message: "a"}
|
||||
|
||||
if err := rs.Add(context.Background(), r1); err != nil {
|
||||
t.Fatalf("Add r1: %v", err)
|
||||
}
|
||||
if err := rs.Add(context.Background(), r2); err != nil {
|
||||
t.Fatalf("Add r2: %v", err)
|
||||
}
|
||||
if err := rs.Add(context.Background(), r2); !errors.Is(err, ErrDuplicate) {
|
||||
t.Fatalf("duplicate: err = %v, want ErrDuplicate", err)
|
||||
}
|
||||
|
||||
list := rs.List()
|
||||
if len(list) != 2 {
|
||||
t.Fatalf("len(List) = %d, want 2", len(list))
|
||||
}
|
||||
// Sorted by At ascending.
|
||||
if list[0].ID != "sooner" || list[1].ID != "later" {
|
||||
t.Fatalf("not sorted by At: %+v", list)
|
||||
}
|
||||
|
||||
if err := rs.Cancel(context.Background(), "sooner"); err != nil {
|
||||
t.Fatalf("Cancel: %v", err)
|
||||
}
|
||||
if got := len(rs.List()); got != 1 {
|
||||
t.Fatalf("len(List) after cancel = %d, want 1", got)
|
||||
}
|
||||
if err := rs.Cancel(context.Background(), "sooner"); !errors.Is(err, ErrNotFound) {
|
||||
t.Fatalf("cancel missing: err = %v, want ErrNotFound", err)
|
||||
}
|
||||
if err := rs.Add(context.Background(), Reminder{ID: "", At: base}); !errors.Is(err, ErrInvalidRule) {
|
||||
t.Fatalf("empty id: err = %v, want ErrInvalidRule", err)
|
||||
}
|
||||
if err := rs.Add(context.Background(), Reminder{ID: "x"}); !errors.Is(err, ErrInvalidRule) {
|
||||
t.Fatalf("zero At: err = %v, want ErrInvalidRule", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReminder_FiresOnDue(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := bus.New()
|
||||
defer b.Close()
|
||||
|
||||
got := make(chan bus.Event, 1)
|
||||
unsub, err := b.Subscribe(context.Background(), SubjectTrigger, func(_ context.Context, ev bus.Event) error {
|
||||
got <- ev
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("subscribe: %v", err)
|
||||
}
|
||||
defer unsub()
|
||||
|
||||
rs, err := NewReminder(Config{ReminderInterval: 10 * time.Millisecond}, Deps{Bus: b})
|
||||
if err != nil {
|
||||
t.Fatalf("NewReminder: %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
r := Reminder{ID: "wake", At: time.Now().Add(20 * time.Millisecond), Message: "time to focus"}
|
||||
if err := rs.Add(ctx, r); err != nil {
|
||||
t.Fatalf("Add: %v", err)
|
||||
}
|
||||
if err := rs.Start(ctx); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case ev := <-got:
|
||||
rem, ok := ev.Payload.(Reminder)
|
||||
if !ok {
|
||||
t.Fatalf("payload type = %T, want Reminder", ev.Payload)
|
||||
}
|
||||
if rem.ID != "wake" || rem.Message != "time to focus" {
|
||||
t.Fatalf("reminder = %+v", rem)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("reminder did not fire")
|
||||
}
|
||||
if err := rs.Stop(ctx); err != nil {
|
||||
t.Fatalf("Stop: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReminder_StartStopIdempotent(t *testing.T) {
|
||||
t.Parallel()
|
||||
rs, err := NewReminder(Config{}, Deps{Bus: bus.New()})
|
||||
if err != nil {
|
||||
t.Fatalf("NewReminder: %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
if err := rs.Start(ctx); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
if err := rs.Start(ctx); err != nil {
|
||||
t.Fatalf("second Start: %v", err)
|
||||
}
|
||||
if err := rs.Stop(ctx); err != nil {
|
||||
t.Fatalf("Stop: %v", err)
|
||||
}
|
||||
if err := rs.Stop(ctx); err != nil {
|
||||
t.Fatalf("second Stop: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReminder_ConcurrentAddCancelList(t *testing.T) {
|
||||
t.Parallel()
|
||||
rs, err := NewReminder(Config{}, Deps{Bus: bus.New()})
|
||||
if err != nil {
|
||||
t.Fatalf("NewReminder: %v", err)
|
||||
}
|
||||
const n = 50
|
||||
done := make(chan struct{})
|
||||
for i := 0; i < n; i++ {
|
||||
go func(i int) {
|
||||
defer func() { done <- struct{}{} }()
|
||||
id := "r"
|
||||
_ = rs.Add(context.Background(), Reminder{ID: id, At: time.Now().Add(time.Hour), Message: "m"})
|
||||
_ = rs.List()
|
||||
_ = rs.Cancel(context.Background(), id)
|
||||
}(i)
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_ReminderLifecycle(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := bus.New()
|
||||
defer b.Close()
|
||||
s, err := NewService(Config{}, Deps{Bus: b})
|
||||
if err != nil {
|
||||
t.Fatalf("NewService: %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
if err := s.AddReminder(ctx, Reminder{ID: "x", At: time.Now().Add(time.Hour), Message: "m"}); err != nil {
|
||||
t.Fatalf("AddReminder: %v", err)
|
||||
}
|
||||
if got := len(s.Reminders()); got != 1 {
|
||||
t.Fatalf("len(Reminders) = %d, want 1", got)
|
||||
}
|
||||
if err := s.CancelReminder(ctx, "x"); err != nil {
|
||||
t.Fatalf("CancelReminder: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user