Files
Cyrene_v2/internal/services/automations/action_test.go
T

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")
}
}