package ask
import (
"context"
"errors"
"strings"
"testing"
"codey/internal/hooks"
)
// recordHooks attaches a recorder and returns the payloads it will fill.
func recordHooks(a *Asker) *[]hooks.Payload {
var got []hooks.Payload
a.Hooks = hooks.New("s1")
a.Hooks.Add(hooks.HandlerFunc(func(_ context.Context, p hooks.Payload) error {
got = append(got, p)
return nil
}))
return &got
}
func TestAskBracketsThePromptWithBlockedAndUnblocked(t *testing.T) {
tests := []struct {
name string
promptErr error
}{
{name: "answered"},
{name: "prompt failed", promptErr: errors.New("turn cancelled")},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// The prompt writes into the same sequence as the hooks, so the
// test can tell "Blocked before the human was asked" from
// "Blocked at some point".
var order []string
a := NewAsker(func(context.Context, Request) (Response, error) {
order = append(order, "prompt")
return Response{}, tc.promptErr
})
a.Hooks = hooks.New("s1")
a.Hooks.Add(hooks.HandlerFunc(func(_ context.Context, p hooks.Payload) error {
order = append(order, string(p.Event))
return nil
}))
_, err := a.Ask(context.Background(), Request{Questions: []Question{twoOptions()}})
if !errors.Is(err, tc.promptErr) {
t.Fatalf("Ask = %v, want the prompt's own error %v", err, tc.promptErr)
}
if want := "blocked prompt unblocked"; strings.Join(order, " ") != want {
t.Errorf("order = %v, want %q", order, want)
}
})
}
}
func TestBlockedNamesTheQuestionsWithoutRepeatingThem(t *testing.T) {
a := NewAsker(func(context.Context, Request) (Response, error) { return Response{}, nil })
got := recordHooks(a)
scope := twoOptions()
scope.Header = "Scope"
scope.Question = "Should this cover the TUI too?"
req := Request{Agent: "a3", Questions: []Question{twoOptions(), scope}}
if _, err := a.Ask(context.Background(), req); err != nil {
t.Fatalf("Ask: %v", err)
}
blocked := (*got)[0]
if blocked.Reason != hooks.ReasonQuestion {
t.Errorf("Reason = %q, want %q", blocked.Reason, hooks.ReasonQuestion)
}
for _, header := range []string{"Storage", "Scope"} {
if !strings.Contains(blocked.Message, header) {
t.Errorf("Message %q does not name the %s question", blocked.Message, header)
}
}
// The headers name the decisions; the questions carry the conversation,
// which is not a bystander's to have.
if strings.Contains(blocked.Message, "How should sessions be stored?") {
t.Errorf("Message %q repeats the question text", blocked.Message)
}
for _, p := range *got {
if p.Agent != "a3" {
t.Errorf("%s.Agent = %q, want the asking subagent", p.Event, p.Agent)
}
}
if (*got)[1].Reason != hooks.ReasonQuestion {
t.Errorf("Unblocked.Reason = %q, want %q so a watcher can pair it with the Blocked", (*got)[1].Reason, hooks.ReasonQuestion)
}
}
// With nothing to ask, nobody is waiting: a Blocked with no Unblocked to
// follow would be exactly the stuck state the events exist to prevent.
func TestNoPromptFiresNothing(t *testing.T) {
a := NewAsker(nil)
got := recordHooks(a)
if _, err := a.Ask(context.Background(), Request{Questions: []Question{twoOptions()}}); err == nil {
t.Fatal("an asker with nothing installed answered")
}
if len(*got) != 0 {
t.Errorf("fired %v with no prompt installed", *got)
}
}