package ask import ( "context" "errors" "strings" "testing" "github.com/johannSo/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) } }