ai-agent/internal/tui/model_test.go
admin 8dc496b626
Some checks failed
CI / test (push) Has been cancelled
Release / release (push) Failing after 4m36s
first commit
2026-03-08 15:40:34 +07:00

407 lines
14 KiB
Go

package tui
import (
"strings"
"testing"
"time"
"ai-agent/internal/command"
tea "charm.land/bubbletea/v2"
)
func TestSubmitInput_EmptyReturnsNil(t *testing.T) {
m := newTestModel(t)
cmd := m.submitInput()
if cmd != nil {
t.Error("submitInput with empty input should return nil")
}
}
func TestHelp_OnlyWhenIdleAndEmpty(t *testing.T) {
t.Run("idle_empty_opens_help", func(t *testing.T) {
m := newTestModel(t)
m.state = StateIdle
updated, _ := m.Update(charKey('?'))
m = updated.(*Model)
if m.overlay != OverlayHelp {
t.Errorf("? with idle+empty should open help, got overlay=%d", m.overlay)
}
})
t.Run("idle_nonempty_no_help", func(t *testing.T) {
m := newTestModel(t)
m.state = StateIdle
m.input.SetValue("hello")
updated, _ := m.Update(charKey('?'))
m = updated.(*Model)
if m.overlay == OverlayHelp {
t.Error("? with non-empty input should not open help")
}
})
t.Run("waiting_no_help", func(t *testing.T) {
m := newTestModel(t)
m.state = StateWaiting
updated, _ := m.Update(charKey('?'))
m = updated.(*Model)
if m.overlay == OverlayHelp {
t.Error("? in StateWaiting should not open help")
}
})
}
func TestToggleTools_OnlyWhenIdleAndEmpty(t *testing.T) {
t.Run("idle_empty_toggles", func(t *testing.T) {
m := newTestModel(t)
m.state = StateIdle
before := m.toolsCollapsed
updated, _ := m.Update(charKey('t'))
m = updated.(*Model)
if m.toolsCollapsed == before {
t.Error("'t' with idle+empty should toggle toolsCollapsed")
}
})
t.Run("idle_nonempty_no_toggle", func(t *testing.T) {
m := newTestModel(t)
m.state = StateIdle
m.input.SetValue("hello")
before := m.toolsCollapsed
updated, _ := m.Update(charKey('t'))
m = updated.(*Model)
if m.toolsCollapsed != before {
t.Error("'t' with non-empty input should not toggle tools")
}
})
}
func TestESC_CancelOnlyWhenStreamingOrWaiting(t *testing.T) {
t.Run("idle_no_cancel", func(t *testing.T) {
m := newTestModel(t)
m.state = StateIdle
cancelCalled := false
m.cancel = func() { cancelCalled = true }
updated, _ := m.Update(escKey())
_ = updated.(*Model)
if cancelCalled {
t.Error("ESC in idle should not call cancel")
}
})
t.Run("streaming_cancels", func(t *testing.T) {
m := newTestModel(t)
m.state = StateStreaming
cancelCalled := false
m.cancel = func() { cancelCalled = true }
updated, _ := m.Update(escKey())
_ = updated.(*Model)
if !cancelCalled {
t.Error("ESC in streaming should call cancel")
}
})
t.Run("waiting_cancels", func(t *testing.T) {
m := newTestModel(t)
m.state = StateWaiting
cancelCalled := false
m.cancel = func() { cancelCalled = true }
updated, _ := m.Update(escKey())
_ = updated.(*Model)
if !cancelCalled {
t.Error("ESC in waiting should call cancel")
}
})
}
func TestSystemMessageMsg_AppendsEntry(t *testing.T) {
m := newTestModel(t)
before := len(m.entries)
updated, _ := m.Update(SystemMessageMsg{Msg: "hello system"})
m = updated.(*Model)
if len(m.entries) != before+1 {
t.Fatalf("expected %d entries, got %d", before+1, len(m.entries))
}
last := m.entries[len(m.entries)-1]
if last.Kind != "system" {
t.Errorf("expected kind 'system', got %q", last.Kind)
}
if last.Content != "hello system" {
t.Errorf("expected content 'hello system', got %q", last.Content)
}
}
func TestErrorMsg_AppendsEntry(t *testing.T) {
m := newTestModel(t)
before := len(m.entries)
updated, _ := m.Update(ErrorMsg{Msg: "something broke"})
m = updated.(*Model)
if len(m.entries) != before+1 {
t.Fatalf("expected %d entries, got %d", before+1, len(m.entries))
}
last := m.entries[len(m.entries)-1]
if last.Kind != "error" {
t.Errorf("expected kind 'error', got %q", last.Kind)
}
if last.Content != "something broke" {
t.Errorf("expected content 'something broke', got %q", last.Content)
}
}
func TestToolCallResultMsg(t *testing.T) {
t.Run("updates_tool_entry", func(t *testing.T) {
m := newTestModel(t)
m.toolEntries = append(m.toolEntries, ToolEntry{
Name: "read_file",
Status: ToolStatusRunning,
})
m.toolsPending = 1
updated, _ := m.Update(ToolCallResultMsg{
Name: "read_file",
Result: "file contents",
IsError: false,
Duration: 42 * time.Millisecond,
})
m = updated.(*Model)
if m.toolEntries[0].Status != ToolStatusDone {
t.Errorf("expected ToolStatusDone, got %d", m.toolEntries[0].Status)
}
if m.toolEntries[0].Result != "file contents" {
t.Errorf("expected 'file contents', got %q", m.toolEntries[0].Result)
}
if m.toolsPending != 0 {
t.Errorf("toolsPending should be 0, got %d", m.toolsPending)
}
})
t.Run("truncates_long_result", func(t *testing.T) {
m := newTestModel(t)
m.toolEntries = append(m.toolEntries, ToolEntry{
Name: "read_file",
Status: ToolStatusRunning,
})
longResult := strings.Repeat("x", 2500)
updated, _ := m.Update(ToolCallResultMsg{
Name: "read_file",
Result: longResult,
})
m = updated.(*Model)
if len(m.toolEntries[0].Result) != 2000 {
t.Errorf("result should be truncated to 2000, got %d", len(m.toolEntries[0].Result))
}
if !strings.HasSuffix(m.toolEntries[0].Result, "...") {
t.Error("truncated result should end with '...'")
}
})
t.Run("error_status", func(t *testing.T) {
m := newTestModel(t)
m.toolEntries = append(m.toolEntries, ToolEntry{
Name: "exec",
Status: ToolStatusRunning,
})
updated, _ := m.Update(ToolCallResultMsg{
Name: "exec",
Result: "command failed",
IsError: true,
})
m = updated.(*Model)
if m.toolEntries[0].Status != ToolStatusError {
t.Errorf("expected ToolStatusError, got %d", m.toolEntries[0].Status)
}
if !m.toolEntries[0].IsError {
t.Error("IsError should be true")
}
})
}
func TestAgentDoneMsg(t *testing.T) {
m := newTestModel(t)
m.state = StateStreaming
m.userScrolledUp = true
m.anchorActive = false
updated, _ := m.Update(AgentDoneMsg{})
m = updated.(*Model)
if m.state != StateIdle {
t.Errorf("state should be StateIdle, got %d", m.state)
}
if m.userScrolledUp {
t.Error("userScrolledUp should be reset to false")
}
if !m.anchorActive {
t.Error("anchorActive should be reset to true")
}
}
func TestInitCompleteMsg(t *testing.T) {
t.Run("basic_fields", func(t *testing.T) {
m := newTestModel(t)
updated, _ := m.Update(InitCompleteMsg{
Model: "llama3",
ModelList: []string{"llama3", "qwen3"},
AgentProfile: "default",
AgentList: []string{"default", "coder"},
ToolCount: 5,
ServerCount: 2,
NumCtx: 8192,
})
m = updated.(*Model)
if m.model != "llama3" {
t.Errorf("model should be 'llama3', got %q", m.model)
}
if len(m.modelList) != 2 {
t.Errorf("modelList should have 2 items, got %d", len(m.modelList))
}
if m.toolCount != 5 {
t.Errorf("toolCount should be 5, got %d", m.toolCount)
}
if m.serverCount != 2 {
t.Errorf("serverCount should be 2, got %d", m.serverCount)
}
})
t.Run("with_failed_servers", func(t *testing.T) {
m := newTestModel(t)
before := len(m.entries)
updated, _ := m.Update(InitCompleteMsg{
Model: "llama3",
FailedServers: []FailedServer{
{Name: "server1", Reason: "timeout"},
},
})
m = updated.(*Model)
if len(m.entries) != before+1 {
t.Fatalf("should append system entry for failed servers, got %d entries", len(m.entries))
}
last := m.entries[len(m.entries)-1]
if last.Kind != "system" {
t.Errorf("expected kind 'system', got %q", last.Kind)
}
if !strings.Contains(last.Content, "server1") {
t.Errorf("should contain server name, got %q", last.Content)
}
})
}
func TestHandleCommandAction(t *testing.T) {
tests := []struct {
name string
result command.Result
check func(t *testing.T, m *Model, cmd tea.Cmd)
}{
{
name: "ActionShowHelp",
result: command.Result{Action: command.ActionShowHelp},
check: func(t *testing.T, m *Model, cmd tea.Cmd) {
if m.overlay != OverlayHelp {
t.Errorf("expected OverlayHelp, got %d", m.overlay)
}
},
},
{
name: "ActionClear_with_text",
result: command.Result{Action: command.ActionClear, Text: "Cleared."},
check: func(t *testing.T, m *Model, cmd tea.Cmd) {
if len(m.entries) != 1 {
t.Errorf("expected 1 entry, got %d", len(m.entries))
}
if m.entries[0].Kind != "system" {
t.Errorf("expected system entry, got %q", m.entries[0].Kind)
}
},
},
{
name: "ActionQuit",
result: command.Result{Action: command.ActionQuit},
check: func(t *testing.T, m *Model, cmd tea.Cmd) {
if cmd == nil {
t.Error("ActionQuit should return a cmd (tea.Quit)")
}
},
},
{
name: "ActionLoadContext",
result: command.Result{Action: command.ActionLoadContext, Data: "test.md\x00# Hello", Text: "Loaded."},
check: func(t *testing.T, m *Model, cmd tea.Cmd) {
if m.loadedFile != "test.md" {
t.Errorf("expected loadedFile='test.md', got %q", m.loadedFile)
}
},
},
{
name: "ActionUnloadContext",
result: command.Result{Action: command.ActionUnloadContext, Text: "Unloaded."},
check: func(t *testing.T, m *Model, cmd tea.Cmd) {
if m.loadedFile != "" {
t.Errorf("expected empty loadedFile, got %q", m.loadedFile)
}
},
},
{
name: "ActionSwitchModel",
result: command.Result{Action: command.ActionSwitchModel, Data: "gpt-4", Text: "Switched."},
check: func(t *testing.T, m *Model, cmd tea.Cmd) {
if m.model != "gpt-4" {
t.Errorf("expected model='gpt-4', got %q", m.model)
}
},
},
{
name: "ActionSwitchAgent",
result: command.Result{Action: command.ActionSwitchAgent, Data: "coder", Text: "Switched."},
check: func(t *testing.T, m *Model, cmd tea.Cmd) {
if m.agentProfile != "coder" {
t.Errorf("expected agentProfile='coder', got %q", m.agentProfile)
}
},
},
{
name: "ActionNone_with_text",
result: command.Result{Action: command.ActionNone, Text: "Info message"},
check: func(t *testing.T, m *Model, cmd tea.Cmd) {
if len(m.entries) == 0 {
t.Fatal("expected at least one entry")
}
last := m.entries[len(m.entries)-1]
if last.Content != "Info message" {
t.Errorf("expected 'Info message', got %q", last.Content)
}
},
},
{
name: "ActionNone_empty_text",
result: command.Result{Action: command.ActionNone, Text: ""},
check: func(t *testing.T, m *Model, cmd tea.Cmd) {
// Should not add any entry.
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := newTestModel(t)
if tt.result.Action == command.ActionUnloadContext {
m.loadedFile = "old.md"
}
cmd := m.handleCommandAction(tt.result)
tt.check(t, m, cmd)
})
}
}
func TestCommandResultMsg(t *testing.T) {
t.Run("with_text", func(t *testing.T) {
m := newTestModel(t)
before := len(m.entries)
updated, _ := m.Update(CommandResultMsg{Text: "Result info"})
m = updated.(*Model)
if len(m.entries) != before+1 {
t.Fatalf("expected %d entries, got %d", before+1, len(m.entries))
}
if m.entries[len(m.entries)-1].Content != "Result info" {
t.Errorf("expected 'Result info', got %q", m.entries[len(m.entries)-1].Content)
}
})
t.Run("empty_text_no_entry", func(t *testing.T) {
m := newTestModel(t)
before := len(m.entries)
updated, _ := m.Update(CommandResultMsg{Text: ""})
m = updated.(*Model)
if len(m.entries) != before {
t.Errorf("expected %d entries (no change), got %d", before, len(m.entries))
}
})
}