Vectrum
Sign In
105 lines · 3.3 KB · Go
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)
	}
}