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