package tui import ( "context" "strings" "testing" "time" ) func TestAskUser_BlocksUntilAnswered(t *testing.T) { m := newTestModel(t) m.askCh = make(chan askRequest) resultCh := make(chan string, 1) errCh := make(chan error, 1) go func() { answer, err := m.askUser(context.Background(), "which format?", []string{"json", "yaml"}) resultCh <- answer errCh <- err }() var req askRequest select { case req = <-m.askCh: case <-time.After(time.Second): t.Fatal("timed out waiting for the request on askCh") } if req.question != "which format?" { t.Errorf("req.question = %q, want %q", req.question, "which format?") } if len(req.options) != 2 || req.options[0] != "json" || req.options[1] != "yaml" { t.Errorf("req.options = %v", req.options) } select { case <-resultCh: t.Fatal("askUser returned before the response was sent") case <-time.After(50 * time.Millisecond): } req.resp <- "json please" select { case got := <-resultCh: if got != "json please" { t.Errorf("askUser returned %q, want %q", got, "json please") } if err := <-errCh; err != nil { t.Errorf("unexpected error: %v", err) } case <-time.After(time.Second): t.Fatal("timed out waiting for askUser to return") } } func TestAskUser_ContextCancelReturnsErrorWithoutHanging(t *testing.T) { m := newTestModel(t) m.askCh = make(chan askRequest) // niemand liest daraus ctx, cancel := context.WithCancel(context.Background()) errCh := make(chan error, 1) go func() { _, err := m.askUser(ctx, "irrelevant", nil) errCh <- err }() cancel() select { case err := <-errCh: if err == nil { t.Error("expected a cancelled context to return an error") } case <-time.After(time.Second): t.Fatal("askUser did not return after context cancellation") } } func TestUpdate_AskRequestMsgOpensPromptAndReListens(t *testing.T) { m := newTestModel(t) m.askCh = make(chan askRequest, 1) req := askRequest{question: "which format?", options: []string{"json", "yaml"}, resp: make(chan string, 1)} _, cmd := m.Update(askRequestMsg{req: req}) if m.askReq == nil || m.askReq.question != "which format?" { t.Fatalf("expected askReq to be set to the incoming request, got %+v", m.askReq) } if cmd == nil { t.Fatal("expected Update to re-arm listenAskRequests") } if len(m.entries) != 1 || m.entries[0].kind != entryQuestion { t.Fatalf("expected the request to appear inline in the transcript, got %+v", m.entries) } if !strings.Contains(m.entries[0].text, "which format?") { t.Errorf("question entry should contain the question, got: %q", m.entries[0].text) } if m.textarea.Placeholder != askInputPlaceholder { t.Errorf("expected the placeholder to switch to askInputPlaceholder, got %q", m.textarea.Placeholder) } } func TestSubmit_WithOpenAskRequestAnswersInsteadOfSendingAMessage(t *testing.T) { m := newTestModel(t) resp := make(chan string, 1) m.askReq = &askRequest{question: "which format?", resp: resp} cmd := m.submit("json please", false) if cmd != nil { t.Error("expected submit to not return a chat/command cmd while answering a question") } if m.askReq != nil { t.Error("expected askReq to be cleared") } select { case got := <-resp: if got != "json please" { t.Errorf("resp received %q, want %q", got, "json please") } default: t.Fatal("expected the answer to be sent on resp") } if m.textarea.Placeholder != defaultInputPlaceholder { t.Errorf("expected the placeholder to reset to defaultInputPlaceholder, got %q", m.textarea.Placeholder) } if len(m.history) != 0 { t.Error("expected the answer to not be recorded as chat history") } } func TestView_AskRequestDoesNotTakeOverTheScreen(t *testing.T) { m := newTestModelWithStore(t) // renderStatusLine liest m.store.ID m.ready = true m.width, m.height = 80, 24 m.viewport.Width, m.viewport.Height = 80, 20 m.entries = []entry{{kind: entryUser, text: "hello from before"}} m.renderViewport() m.askReq = &askRequest{question: "which format?", resp: make(chan string, 1)} view := m.View() if !strings.Contains(view, m.textarea.View()) { t.Error("expected the input box to remain visible while a question is pending") } if !strings.Contains(stripANSI(view), "hello from before") { t.Error("expected prior transcript content to remain visible, not be replaced by a full-screen overlay") } }