134 lines
3.3 KiB
Go
134 lines
3.3 KiB
Go
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")
|
|
}
|
|
}
|