8cfc590c7e
The clarification modal hard-truncated each question's head line with clip(), so long prompts (e.g. an API question and an Architecture question) were cut at the modal edge and unreadable. Wrap the prompt across rows instead: wrapPrompt() greedily word-wraps with a narrower first line (marker + header chip eat into it) and full-width continuation lines, indented to align under the prompt.
142 lines
4.5 KiB
Go
142 lines
4.5 KiB
Go
package app
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/correx/tui-go/internal/protocol"
|
|
"github.com/correx/tui-go/internal/ws"
|
|
)
|
|
|
|
func clarModel() Model {
|
|
m := NewModel(ws.New("", 0)) // unconnected; Send just buffers
|
|
m.width, m.height = 120, 40
|
|
m.theme = NewTheme(SoftBlue)
|
|
m.selectedID = "s1"
|
|
m.sessionEntered = true
|
|
m.applyServer(protocol.ServerMessage{
|
|
Type: protocol.TypeClarification,
|
|
SessionID: "s1",
|
|
RequestID: "req-1",
|
|
StageID: "analyst",
|
|
Questions: []protocol.ClarificationQuestionDto{
|
|
{ID: "stack", Prompt: "Which stack?", Options: []string{"React", "Vue"}, Header: "Stack"},
|
|
},
|
|
})
|
|
return m
|
|
}
|
|
|
|
func TestClarificationEntersStateAndRenders(t *testing.T) {
|
|
m := clarModel()
|
|
if m.displayState() != StateClarification {
|
|
t.Fatalf("want StateClarification, got %v", m.displayState())
|
|
}
|
|
out := m.clarificationModal()
|
|
for _, want := range []string{"clarifying questions", "Which stack?", "React", "Vue", "Stack", "custom"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Fatalf("modal missing %q:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClarificationLongPromptSoftWraps(t *testing.T) {
|
|
m := clarModel()
|
|
m.width, m.height = 60, 40 // narrow modal so a long prompt must wrap
|
|
longPrompt := "Which API surface should the web UI talk to for streaming session " +
|
|
"events and how should it authenticate against the server endpoints?"
|
|
m.applyServer(protocol.ServerMessage{
|
|
Type: protocol.TypeClarification,
|
|
SessionID: "s1",
|
|
RequestID: "req-2",
|
|
StageID: "analyst",
|
|
Questions: []protocol.ClarificationQuestionDto{
|
|
{ID: "api", Prompt: longPrompt, Options: []string{"REST", "WebSocket"}, Header: "API"},
|
|
},
|
|
})
|
|
out := m.clarificationModal()
|
|
// Every word of the prompt must survive — nothing clipped at the modal edge.
|
|
for _, word := range strings.Fields(longPrompt) {
|
|
if !strings.Contains(out, word) {
|
|
t.Fatalf("wrapped prompt dropped %q:\n%s", word, out)
|
|
}
|
|
}
|
|
// And it must actually span multiple rendered rows (i.e. it wrapped, not overflowed).
|
|
if len(strings.Split(strings.TrimRight(out, "\n"), "\n")) < 4 {
|
|
t.Fatalf("expected the long prompt to wrap across rows:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestWrapPromptFirstLineNarrowerThanContinuation(t *testing.T) {
|
|
lines := wrapPrompt("one two three four five six", 7, 20)
|
|
if lines[0] != "one two" {
|
|
t.Fatalf("first line should honor firstW=7, got %q", lines[0])
|
|
}
|
|
if len(lines) < 2 {
|
|
t.Fatalf("expected continuation lines, got %v", lines)
|
|
}
|
|
}
|
|
|
|
func TestClarificationSelectAndSubmit(t *testing.T) {
|
|
m := clarModel()
|
|
// move the option cursor to "Vue" (index 1) and select it
|
|
m = applyClarKey(m, keyMsg{Type: keyRight})
|
|
m = applyClarKey(m, keyMsg{Type: keySpace})
|
|
if !m.clarChosenAt(0, 1) {
|
|
t.Fatalf("expected option 1 chosen, chosen=%v", m.clarChosen)
|
|
}
|
|
// submit
|
|
m = applyClarKey(m, keyMsg{Type: keyEnter})
|
|
if s := m.session("s1"); s == nil || s.Clar != nil {
|
|
t.Fatalf("expected clarification cleared after submit")
|
|
}
|
|
if m.displayState() != StateInSession {
|
|
t.Fatalf("want StateInSession after submit, got %v", m.displayState())
|
|
}
|
|
}
|
|
|
|
func TestClarificationSubmitGuardWaitsForAllAnswers(t *testing.T) {
|
|
m := clarModel()
|
|
m.applyServer(protocol.ServerMessage{
|
|
Type: protocol.TypeClarification, SessionID: "s1", RequestID: "req-2", StageID: "analyst",
|
|
Questions: []protocol.ClarificationQuestionDto{
|
|
{ID: "a", Prompt: "Q A?", Options: []string{"x"}},
|
|
{ID: "b", Prompt: "Q B?", Options: []string{"y"}},
|
|
},
|
|
})
|
|
// answer only the first question, then attempt submit
|
|
m = applyClarKey(m, keyMsg{Type: keySpace})
|
|
m = applyClarKey(m, keyMsg{Type: keyEnter})
|
|
if s := m.session("s1"); s == nil || s.Clar == nil {
|
|
t.Fatalf("expected clarification to remain with partial answers")
|
|
}
|
|
if m.clarFocus != 1 {
|
|
t.Fatalf("expected focus nudged to first unanswered (1), got %d", m.clarFocus)
|
|
}
|
|
}
|
|
|
|
func TestClarificationCustomTextAnswer(t *testing.T) {
|
|
m := clarModel()
|
|
m = applyClarKey(m, keyMsg{Type: keyRunes, Runes: []rune("e")})
|
|
if !m.clarTyping {
|
|
t.Fatalf("expected typing mode after 'e'")
|
|
}
|
|
for _, r := range "Solid" {
|
|
m = applyClarKey(m, keyMsg{Type: keyRunes, Runes: []rune{r}})
|
|
}
|
|
m = applyClarKey(m, keyMsg{Type: keyEnter})
|
|
if m.clarTyping {
|
|
t.Fatalf("expected typing committed")
|
|
}
|
|
if m.clarTextAt(0) != "Solid" {
|
|
t.Fatalf("want custom text 'Solid', got %q", m.clarTextAt(0))
|
|
}
|
|
if !m.clarAnswered(0, m.session("s1").Clar.Questions[0]) {
|
|
t.Fatalf("expected question answered via custom text")
|
|
}
|
|
}
|
|
|
|
func applyClarKey(m Model, k keyMsg) Model {
|
|
updated, _ := m.handleClarificationKey(k)
|
|
return updated.(Model)
|
|
}
|