mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
test(ui): add 23 unit tests for AppModel state machine (TAS-33)
Cover state transitions (input->working->approval->input), StepError->input, ESC cancel flow (single tap sets canceling, double tap calls CancelCurrentStep, timer expiry resets), queue badge updates, window resize/distributeHeight, and tea.Println cmd emission on StepCompleteEvent/StepErrorEvent.
This commit is contained in:
@@ -0,0 +1,553 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/fantasy"
|
||||
"github.com/mark3labs/mcphost/internal/app"
|
||||
)
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Stub AppController
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// stubAppController is a minimal implementation of AppController for tests.
|
||||
// It records which methods were called and allows the test to inspect the results.
|
||||
type stubAppController struct {
|
||||
runCalls []string
|
||||
cancelCalled int
|
||||
clearQueueCalled int
|
||||
clearMsgCalled int
|
||||
queueLen int
|
||||
}
|
||||
|
||||
func (s *stubAppController) Run(prompt string) {
|
||||
s.runCalls = append(s.runCalls, prompt)
|
||||
}
|
||||
|
||||
func (s *stubAppController) CancelCurrentStep() {
|
||||
s.cancelCalled++
|
||||
}
|
||||
|
||||
func (s *stubAppController) QueueLength() int {
|
||||
return s.queueLen
|
||||
}
|
||||
|
||||
func (s *stubAppController) ClearQueue() {
|
||||
s.clearQueueCalled++
|
||||
s.queueLen = 0
|
||||
}
|
||||
|
||||
func (s *stubAppController) ClearMessages() {
|
||||
s.clearMsgCalled++
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Stub child components
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// stubStreamComponent satisfies streamComponentIface without rendering anything.
|
||||
type stubStreamComponent struct {
|
||||
resetCalled int
|
||||
height int
|
||||
lastMsg tea.Msg
|
||||
}
|
||||
|
||||
func (s *stubStreamComponent) Init() tea.Cmd { return nil }
|
||||
func (s *stubStreamComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
s.lastMsg = msg
|
||||
return s, nil
|
||||
}
|
||||
func (s *stubStreamComponent) View() tea.View { return tea.NewView("") }
|
||||
func (s *stubStreamComponent) Reset() { s.resetCalled++ }
|
||||
func (s *stubStreamComponent) SetHeight(h int) { s.height = h }
|
||||
|
||||
// stubInputComponent satisfies inputComponentIface without rendering anything.
|
||||
type stubInputComponent struct {
|
||||
lastMsg tea.Msg
|
||||
}
|
||||
|
||||
func (s *stubInputComponent) Init() tea.Cmd { return nil }
|
||||
func (s *stubInputComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
s.lastMsg = msg
|
||||
return s, nil
|
||||
}
|
||||
func (s *stubInputComponent) View() tea.View { return tea.NewView("") }
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// newTestAppModel creates an AppModel with stub children for unit tests.
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
func newTestAppModel(ctrl AppController) (*AppModel, *stubStreamComponent, *stubInputComponent) {
|
||||
stream := &stubStreamComponent{}
|
||||
input := &stubInputComponent{}
|
||||
m := &AppModel{
|
||||
state: stateInput,
|
||||
appCtrl: ctrl,
|
||||
stream: stream,
|
||||
input: input,
|
||||
renderer: NewMessageRenderer(80, false),
|
||||
compactRdr: NewCompactRenderer(80, false),
|
||||
compactMode: false,
|
||||
modelName: "test-model",
|
||||
width: 80,
|
||||
height: 24,
|
||||
}
|
||||
return m, stream, input
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// sendMsg calls m.Update once with the given message and returns the updated model.
|
||||
func sendMsg(m *AppModel, msg tea.Msg) *AppModel {
|
||||
updated, _ := m.Update(msg)
|
||||
return updated.(*AppModel)
|
||||
}
|
||||
|
||||
// makeTestResponse constructs a fantasy.Response with the given text content.
|
||||
// Uses fantasy.TextContent (the type that ResponseContent.Text() recognises) rather
|
||||
// than TextPart (which is a request-side type).
|
||||
func makeTestResponse(text string) *fantasy.Response {
|
||||
return &fantasy.Response{
|
||||
Content: fantasy.ResponseContent{fantasy.TextContent{Text: text}},
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// State transitions
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// TestStateTransition_InputToWorking verifies that a submitMsg while in
|
||||
// stateInput transitions the model to stateWorking.
|
||||
func TestStateTransition_InputToWorking(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
|
||||
if m.state != stateInput {
|
||||
t.Fatalf("expected stateInput, got %v", m.state)
|
||||
}
|
||||
|
||||
m = sendMsg(m, submitMsg{Text: "hello"})
|
||||
|
||||
if m.state != stateWorking {
|
||||
t.Fatalf("expected stateWorking after submitMsg, got %v", m.state)
|
||||
}
|
||||
if len(ctrl.runCalls) != 1 || ctrl.runCalls[0] != "hello" {
|
||||
t.Fatalf("expected Run called with 'hello', got %v", ctrl.runCalls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStateTransition_WorkingToApproval verifies that a ToolApprovalNeededEvent
|
||||
// while in stateWorking transitions the model to stateApproval and creates the
|
||||
// ApprovalComponent.
|
||||
func TestStateTransition_WorkingToApproval(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
m.state = stateWorking
|
||||
|
||||
responseChan := make(chan bool, 1)
|
||||
m = sendMsg(m, app.ToolApprovalNeededEvent{
|
||||
ToolName: "test_tool",
|
||||
ToolArgs: `{"arg":"val"}`,
|
||||
ResponseChan: responseChan,
|
||||
})
|
||||
|
||||
if m.state != stateApproval {
|
||||
t.Fatalf("expected stateApproval after ToolApprovalNeededEvent, got %v", m.state)
|
||||
}
|
||||
if m.approval == nil {
|
||||
t.Fatal("expected approval component to be set")
|
||||
}
|
||||
if m.approvalChan == nil {
|
||||
t.Fatal("expected approvalChan to be set")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStateTransition_ApprovalToWorking verifies that an approvalResultMsg
|
||||
// while in stateApproval sends the result on approvalChan and transitions back
|
||||
// to stateWorking.
|
||||
func TestStateTransition_ApprovalToWorking(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
m.state = stateApproval
|
||||
|
||||
responseChan := make(chan bool, 1)
|
||||
m.approvalChan = responseChan
|
||||
|
||||
m = sendMsg(m, approvalResultMsg{Approved: true})
|
||||
|
||||
if m.state != stateWorking {
|
||||
t.Fatalf("expected stateWorking after approvalResultMsg, got %v", m.state)
|
||||
}
|
||||
if m.approvalChan != nil {
|
||||
t.Fatal("expected approvalChan to be cleared")
|
||||
}
|
||||
|
||||
// Verify the approval result was sent on the channel.
|
||||
select {
|
||||
case approved := <-responseChan:
|
||||
if !approved {
|
||||
t.Fatal("expected approved=true to be sent on responseChan")
|
||||
}
|
||||
default:
|
||||
t.Fatal("expected a value on responseChan")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStateTransition_WorkingToInput_StepComplete verifies that StepCompleteEvent
|
||||
// transitions from stateWorking back to stateInput and resets the stream component.
|
||||
func TestStateTransition_WorkingToInput_StepComplete(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, stream, _ := newTestAppModel(ctrl)
|
||||
m.state = stateWorking
|
||||
|
||||
m = sendMsg(m, app.StepCompleteEvent{
|
||||
Response: makeTestResponse("all done"),
|
||||
Usage: fantasy.Usage{},
|
||||
})
|
||||
|
||||
if m.state != stateInput {
|
||||
t.Fatalf("expected stateInput after StepCompleteEvent, got %v", m.state)
|
||||
}
|
||||
if stream.resetCalled != 1 {
|
||||
t.Fatalf("expected stream.Reset() called once, got %d", stream.resetCalled)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStateTransition_WorkingToInput_StepError verifies that StepErrorEvent
|
||||
// transitions from stateWorking back to stateInput and resets the stream component.
|
||||
func TestStateTransition_WorkingToInput_StepError(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, stream, _ := newTestAppModel(ctrl)
|
||||
m.state = stateWorking
|
||||
|
||||
m = sendMsg(m, app.StepErrorEvent{Err: errors.New("something broke")})
|
||||
|
||||
if m.state != stateInput {
|
||||
t.Fatalf("expected stateInput after StepErrorEvent, got %v", m.state)
|
||||
}
|
||||
if stream.resetCalled != 1 {
|
||||
t.Fatalf("expected stream.Reset() called once, got %d", stream.resetCalled)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStepError_nilErr verifies that a StepErrorEvent with a nil error does not
|
||||
// panic and still transitions to stateInput.
|
||||
func TestStepError_nilErr(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
m.state = stateWorking
|
||||
|
||||
m = sendMsg(m, app.StepErrorEvent{Err: nil})
|
||||
|
||||
if m.state != stateInput {
|
||||
t.Fatalf("expected stateInput for nil-error StepErrorEvent, got %v", m.state)
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ESC cancel flow
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// TestESCCancel_singleTap verifies that the first ESC press during stateWorking
|
||||
// sets canceling=true (and does not call CancelCurrentStep).
|
||||
func TestESCCancel_singleTap(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
m.state = stateWorking
|
||||
|
||||
m = sendMsg(m, tea.KeyPressMsg{Code: tea.KeyEscape})
|
||||
|
||||
if !m.canceling {
|
||||
t.Fatal("expected canceling=true after first ESC in stateWorking")
|
||||
}
|
||||
if ctrl.cancelCalled != 0 {
|
||||
t.Fatalf("expected no CancelCurrentStep call after first ESC, got %d", ctrl.cancelCalled)
|
||||
}
|
||||
}
|
||||
|
||||
// TestESCCancel_doubleTap verifies that a second ESC press while canceling=true
|
||||
// calls CancelCurrentStep() and resets canceling.
|
||||
func TestESCCancel_doubleTap(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
m.state = stateWorking
|
||||
m.canceling = true // simulate first ESC already pressed
|
||||
|
||||
m = sendMsg(m, tea.KeyPressMsg{Code: tea.KeyEscape})
|
||||
|
||||
if m.canceling {
|
||||
t.Fatal("expected canceling=false after second ESC")
|
||||
}
|
||||
if ctrl.cancelCalled != 1 {
|
||||
t.Fatalf("expected CancelCurrentStep called once after double ESC, got %d", ctrl.cancelCalled)
|
||||
}
|
||||
}
|
||||
|
||||
// TestESCCancel_timerExpiry verifies that cancelTimerExpiredMsg resets canceling
|
||||
// to false (timer fires before second ESC).
|
||||
func TestESCCancel_timerExpiry(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
m.state = stateWorking
|
||||
m.canceling = true
|
||||
|
||||
m = sendMsg(m, cancelTimerExpiredMsg{})
|
||||
|
||||
if m.canceling {
|
||||
t.Fatal("expected canceling=false after timer expiry")
|
||||
}
|
||||
if ctrl.cancelCalled != 0 {
|
||||
t.Fatalf("expected no CancelCurrentStep after timer expiry, got %d", ctrl.cancelCalled)
|
||||
}
|
||||
}
|
||||
|
||||
// TestESCCancel_noEffectInStateInput verifies that ESC in stateInput does not set
|
||||
// canceling (it's passed to child components instead).
|
||||
func TestESCCancel_noEffectInStateInput(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
// state is stateInput by default
|
||||
|
||||
m = sendMsg(m, tea.KeyPressMsg{Code: tea.KeyEscape})
|
||||
|
||||
if m.canceling {
|
||||
t.Fatal("expected canceling=false when ESC pressed in stateInput")
|
||||
}
|
||||
if ctrl.cancelCalled != 0 {
|
||||
t.Fatalf("expected no CancelCurrentStep in stateInput, got %d", ctrl.cancelCalled)
|
||||
}
|
||||
}
|
||||
|
||||
// TestESCCancel_clearedOnStepComplete verifies that completing a step clears
|
||||
// any pending canceling state.
|
||||
func TestESCCancel_clearedOnStepComplete(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
m.state = stateWorking
|
||||
m.canceling = true
|
||||
|
||||
m = sendMsg(m, app.StepCompleteEvent{
|
||||
Response: makeTestResponse("done"),
|
||||
})
|
||||
|
||||
if m.canceling {
|
||||
t.Fatal("expected canceling=false after StepCompleteEvent")
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Queue badge update
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// TestQueueBadge_updatesOnEvent verifies that QueueUpdatedEvent sets queueCount.
|
||||
func TestQueueBadge_updatesOnEvent(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
|
||||
if m.queueCount != 0 {
|
||||
t.Fatalf("expected queueCount=0 initially, got %d", m.queueCount)
|
||||
}
|
||||
|
||||
m = sendMsg(m, app.QueueUpdatedEvent{Length: 3})
|
||||
|
||||
if m.queueCount != 3 {
|
||||
t.Fatalf("expected queueCount=3 after QueueUpdatedEvent, got %d", m.queueCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestQueueBadge_resetsToZero verifies that a QueueUpdatedEvent with Length=0
|
||||
// resets the badge count.
|
||||
func TestQueueBadge_resetsToZero(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
m.queueCount = 5
|
||||
|
||||
m = sendMsg(m, app.QueueUpdatedEvent{Length: 0})
|
||||
|
||||
if m.queueCount != 0 {
|
||||
t.Fatalf("expected queueCount=0 after QueueUpdatedEvent{Length:0}, got %d", m.queueCount)
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Window resize
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// TestWindowResize_updatesWidthHeight verifies that tea.WindowSizeMsg updates
|
||||
// m.width and m.height.
|
||||
func TestWindowResize_updatesWidthHeight(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
|
||||
m = sendMsg(m, tea.WindowSizeMsg{Width: 120, Height: 40})
|
||||
|
||||
if m.width != 120 {
|
||||
t.Fatalf("expected width=120, got %d", m.width)
|
||||
}
|
||||
if m.height != 40 {
|
||||
t.Fatalf("expected height=40, got %d", m.height)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWindowResize_propagatesToStream verifies that tea.WindowSizeMsg is forwarded
|
||||
// to the stream component.
|
||||
func TestWindowResize_propagatesToStream(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, stream, _ := newTestAppModel(ctrl)
|
||||
|
||||
m = sendMsg(m, tea.WindowSizeMsg{Width: 100, Height: 30})
|
||||
|
||||
if stream.lastMsg == nil {
|
||||
t.Fatal("expected stream component to receive WindowSizeMsg")
|
||||
}
|
||||
if _, ok := stream.lastMsg.(tea.WindowSizeMsg); !ok {
|
||||
t.Fatalf("expected stream.lastMsg to be WindowSizeMsg, got %T", stream.lastMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWindowResize_distributeHeight verifies that distributeHeight correctly
|
||||
// sets the stream height after a resize.
|
||||
func TestWindowResize_distributeHeight(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, stream, _ := newTestAppModel(ctrl)
|
||||
|
||||
// With height=30, stream height = 30 - 1 (separator) - 5 (input) = 24
|
||||
m = sendMsg(m, tea.WindowSizeMsg{Width: 80, Height: 30})
|
||||
_ = m
|
||||
|
||||
if stream.height != 24 {
|
||||
t.Fatalf("expected stream height=24, got %d", stream.height)
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// tea.Println on step complete
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// TestStepComplete_printCmd verifies that StepCompleteEvent produces a non-nil
|
||||
// tea.Cmd (the tea.Println call). We verify the Cmd is non-nil rather than
|
||||
// executing it, since tea.Println is a runtime command.
|
||||
func TestStepComplete_printCmd(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
m.state = stateWorking
|
||||
|
||||
_, cmd := m.Update(app.StepCompleteEvent{
|
||||
Response: makeTestResponse("final answer"),
|
||||
Usage: fantasy.Usage{},
|
||||
})
|
||||
|
||||
// A non-nil cmd means printCompletedResponse returned tea.Println(...)
|
||||
if cmd == nil {
|
||||
t.Fatal("expected non-nil cmd (tea.Println) on StepCompleteEvent with response")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStepComplete_nilResponse_noCmd verifies that StepCompleteEvent with a nil
|
||||
// response produces a nil cmd (nothing to print).
|
||||
func TestStepComplete_nilResponse_noCmd(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
m.state = stateWorking
|
||||
|
||||
_, cmd := m.Update(app.StepCompleteEvent{Response: nil})
|
||||
|
||||
if cmd != nil {
|
||||
t.Fatal("expected nil cmd on StepCompleteEvent with nil response")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStepError_printCmd verifies that StepErrorEvent with a non-nil error
|
||||
// produces a non-nil cmd (the tea.Println call for the error message).
|
||||
func TestStepError_printCmd(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
m.state = stateWorking
|
||||
|
||||
_, cmd := m.Update(app.StepErrorEvent{Err: errors.New("agent failed")})
|
||||
|
||||
if cmd == nil {
|
||||
t.Fatal("expected non-nil cmd (tea.Println) on StepErrorEvent with error")
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// SpinnerEvent: stateWorking on Show=true
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// TestSpinnerEvent_showTransitionsToWorking verifies that SpinnerEvent{Show:true}
|
||||
// transitions the model to stateWorking (important for queued step drain path).
|
||||
func TestSpinnerEvent_showTransitionsToWorking(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
// After a step completes, model is back in stateInput. The next queued step
|
||||
// starts and fires SpinnerEvent{Show: true}.
|
||||
m.state = stateInput
|
||||
|
||||
m = sendMsg(m, app.SpinnerEvent{Show: true})
|
||||
|
||||
if m.state != stateWorking {
|
||||
t.Fatalf("expected stateWorking after SpinnerEvent{Show:true} in stateInput, got %v", m.state)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSpinnerEvent_hidDoesNotTransitionState verifies that SpinnerEvent{Show:false}
|
||||
// does not change the state.
|
||||
func TestSpinnerEvent_hideDoesNotTransitionState(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
m.state = stateWorking
|
||||
|
||||
m = sendMsg(m, app.SpinnerEvent{Show: false})
|
||||
|
||||
if m.state != stateWorking {
|
||||
t.Fatalf("expected state to remain stateWorking, got %v", m.state)
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ctrl+c produces tea.Quit
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// TestCtrlC_producesQuit verifies that ctrl+c always returns a tea.Quit cmd.
|
||||
func TestCtrlC_producesQuit(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
|
||||
_, cmd := m.Update(tea.KeyPressMsg{Code: 'c', Mod: tea.ModCtrl})
|
||||
|
||||
if cmd == nil {
|
||||
t.Fatal("expected tea.Quit cmd on ctrl+c, got nil")
|
||||
}
|
||||
// We verify it's a quit command by running it and checking the message type.
|
||||
msg := cmd()
|
||||
if _, ok := msg.(tea.QuitMsg); !ok {
|
||||
t.Fatalf("expected QuitMsg from ctrl+c cmd, got %T", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// submitMsg during stateWorking (queue path)
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// TestSubmit_duringWorking_stays verifies that submitting a new prompt while in
|
||||
// stateWorking keeps the model in stateWorking (queued via app.Run).
|
||||
func TestSubmit_duringWorking_stays(t *testing.T) {
|
||||
ctrl := &stubAppController{}
|
||||
m, _, _ := newTestAppModel(ctrl)
|
||||
m.state = stateWorking
|
||||
|
||||
m = sendMsg(m, submitMsg{Text: "queued prompt"})
|
||||
|
||||
if m.state != stateWorking {
|
||||
t.Fatalf("expected stateWorking to persist after submitMsg during working, got %v", m.state)
|
||||
}
|
||||
if len(ctrl.runCalls) != 1 || ctrl.runCalls[0] != "queued prompt" {
|
||||
t.Fatalf("expected Run('queued prompt') called, got %v", ctrl.runCalls)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user