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,133 @@
|
||||
package automations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
)
|
||||
|
||||
// fakePipeline is an in-memory Pipeline used to observe the prompt an AIAction
|
||||
// sends and to return a canned text. It is the mock for the loop/agent pipeline;
|
||||
// it implements the Pipeline interface, not a concrete loop.
|
||||
type fakePipeline struct {
|
||||
mu sync.Mutex
|
||||
text string
|
||||
err error
|
||||
msgs []model.Message
|
||||
calls int
|
||||
}
|
||||
|
||||
func (f *fakePipeline) Run(ctx context.Context, sessionID string, msgs []model.Message) (*loop.Result, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.calls++
|
||||
f.msgs = append(f.msgs, msgs...)
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
return &loop.Result{Text: f.text, StopReason: "stop", Steps: 1}, nil
|
||||
}
|
||||
|
||||
func (f *fakePipeline) lastMessage() model.Message {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if len(f.msgs) == 0 {
|
||||
return model.Message{}
|
||||
}
|
||||
return f.msgs[len(f.msgs)-1]
|
||||
}
|
||||
|
||||
func TestAIAction_RunsPipelineAndPublishesResult(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()
|
||||
|
||||
p := &fakePipeline{text: "hello from pipeline"}
|
||||
act, err := NewAIAction("gen_greeting", "You are a greeter", "sess-1", p)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAIAction: %v", err)
|
||||
}
|
||||
|
||||
s, err := New(Config{}, Deps{Bus: b})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %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)
|
||||
}
|
||||
|
||||
var res Result
|
||||
select {
|
||||
case ev := <-got:
|
||||
v, ok := ev.Payload.(Result)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected payload type: %T", ev.Payload)
|
||||
}
|
||||
res = v
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("AI action did not fire")
|
||||
}
|
||||
|
||||
if res.Kind != "schedule" {
|
||||
t.Errorf("kind = %q, want schedule", res.Kind)
|
||||
}
|
||||
if res.Action != "gen_greeting" {
|
||||
t.Errorf("action = %q, want gen_greeting", res.Action)
|
||||
}
|
||||
if res.Output != "hello from pipeline" {
|
||||
t.Errorf("output = %#v, want hello from pipeline", res.Output)
|
||||
}
|
||||
last := p.lastMessage()
|
||||
if len(last.Content) == 0 || last.Content[0].Text != "You are a greeter" {
|
||||
t.Errorf("prompt = %#v, want the configured prompt", last.Content)
|
||||
}
|
||||
if p.calls < 1 {
|
||||
t.Errorf("pipeline not called")
|
||||
}
|
||||
|
||||
if err := s.Stop(ctx); err != nil {
|
||||
t.Fatalf("Stop: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIAction_RequiresPipeline(t *testing.T) {
|
||||
t.Parallel()
|
||||
if _, err := NewAIAction("x", "p", "s", nil); err == nil {
|
||||
t.Fatal("expected error for nil pipeline")
|
||||
} else if !errors.Is(err, ErrNoPipeline) {
|
||||
t.Fatalf("err = %v, want ErrNoPipeline", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIAction_PropagatesPipelineError(t *testing.T) {
|
||||
t.Parallel()
|
||||
p := &fakePipeline{err: context.DeadlineExceeded}
|
||||
act, err := NewAIAction("x", "p", "s", p)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAIAction: %v", err)
|
||||
}
|
||||
if _, err := act.Execute(context.Background(), nil); err == nil {
|
||||
t.Fatal("expected error from pipeline")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user