110 lines
3.6 KiB
Go
110 lines
3.6 KiB
Go
package usecase
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"go.maximumdirect.net/promptkit/internal/domain"
|
|
)
|
|
|
|
func TestCoordinateBudgetAndDelivery(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
budget int
|
|
stop bool
|
|
outcome CoordinationOutcome
|
|
runs, callbacks int
|
|
}{
|
|
{"zero stop", 0, true, CoordinationStopped, 0, 1},
|
|
{"zero exhausted", 0, false, CoordinationExhausted, 0, 1},
|
|
{"stop after last", 2, true, CoordinationStopped, 2, 3},
|
|
{"request after last", 2, false, CoordinationExhausted, 2, 3},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
start := domain.CaptureExchange(nil, "initial", nil, "start")
|
|
runs, callbacks := 0, 0
|
|
result, err := Coordinate(context.Background(), start, tc.budget, func(_ context.Context, turn ConversationTurn[string]) (*string, error) {
|
|
callbacks++
|
|
if turn.RemainingTurns != tc.budget-runs || (runs == 0 && turn.Result != nil) || (runs > 0 && (turn.Result == nil || *turn.Result != fmt.Sprint(runs))) {
|
|
t.Fatalf("unexpected turn: %#v", turn)
|
|
}
|
|
if tc.stop && turn.RemainingTurns == 0 {
|
|
return nil, nil
|
|
}
|
|
value := "next"
|
|
return &value, nil
|
|
}, func(_ context.Context, current *domain.Exchange, _ *string) (*string, *domain.Exchange, error) {
|
|
runs++
|
|
value := fmt.Sprint(runs)
|
|
return &value, domain.CaptureExchange(current, value, nil, value), nil
|
|
})
|
|
if err != nil || result.Outcome != tc.outcome || result.Undelivered != nil || runs != tc.runs || callbacks != tc.callbacks {
|
|
t.Fatalf("result=%#v error=%v runs=%d callbacks=%d", result, err, runs, callbacks)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCoordinateCancellationAndErrors(t *testing.T) {
|
|
start := domain.CaptureExchange(nil, "initial", nil, "start")
|
|
request := "continue"
|
|
for _, mode := range []string{"before", "after run", "inside callback", "callback error", "run error"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
sentinel := errors.New("sentinel")
|
|
calls := 0
|
|
if mode == "before" {
|
|
cancel()
|
|
}
|
|
result, err := Coordinate(ctx, start, 1, func(_ context.Context, turn ConversationTurn[string]) (*string, error) {
|
|
calls++
|
|
if mode == "inside callback" && turn.Result != nil {
|
|
cancel()
|
|
return nil, sentinel
|
|
}
|
|
if mode == "callback error" {
|
|
return nil, fmt.Errorf("callback: %w", sentinel)
|
|
}
|
|
return &request, nil
|
|
}, func(_ context.Context, current *domain.Exchange, _ *string) (*string, *domain.Exchange, error) {
|
|
if mode == "run error" {
|
|
return nil, nil, fmt.Errorf("run: %w", sentinel)
|
|
}
|
|
value := "response"
|
|
if mode == "after run" {
|
|
cancel()
|
|
}
|
|
return &value, domain.CaptureExchange(current, "next", nil, value), nil
|
|
})
|
|
if mode == "before" || mode == "after run" || mode == "inside callback" {
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
} else if !errors.Is(err, sentinel) {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
if result.Outcome != "" || (mode == "after run") != (result.Undelivered != nil) {
|
|
t.Fatalf("result = %#v", result)
|
|
}
|
|
if mode == "before" && calls != 0 {
|
|
t.Fatal("callback invoked after cancellation")
|
|
}
|
|
if mode == "inside callback" && calls != 2 {
|
|
t.Fatalf("callbacks = %d", calls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCoordinatePropagatesPanic(t *testing.T) {
|
|
defer func() {
|
|
if recover() != "callback panic" {
|
|
t.Error("panic did not propagate")
|
|
}
|
|
}()
|
|
Coordinate[string, string](context.Background(), nil, 0, func(context.Context, ConversationTurn[string]) (*string, error) { panic("callback panic") }, nil)
|
|
}
|