picoclaw/pkg/tui/model_test.go

294 lines
6.6 KiB
Go

// PicoClaw - Ultra-lightweight personal AI agent
// License: MIT
package tui
import (
"fmt"
"strings"
"testing"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
)
func TestTruncateSessionKey(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "short key unchanged",
input: "abc",
want: "abc",
},
{
name: "exact 15 chars unchanged",
input: "123456789012345",
want: "123456789012345",
},
{
name: "long key truncated with ellipsis",
input: "1234567890123456",
want: "123456789012345…",
},
{
name: "empty key",
input: "",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := truncateSessionKey(tt.input)
if got != tt.want {
t.Errorf("truncateSessionKey(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
// newTestModel creates a Model for testing without requiring a real AgentLoop.
// The model is constructed directly, bypassing NewModel. We must use
// textarea.New() because the bubbles textarea panics on SetWidth if its
// internal state is nil.
func newTestModel() Model {
ta := textarea.New()
ta.SetHeight(textareaHeight)
return Model{
textarea: ta,
sessionKey: "test-session",
modelName: "test-model",
messages: []chatMessage{},
eventBridge: newEventBridge(),
}
}
// initTestModel creates a test model and sends a WindowSizeMsg to make it ready.
func initTestModel(t *testing.T) Model {
t.Helper()
m := newTestModel()
result, _ := m.Update(tea.WindowSizeMsg{Width: 80, Height: 24})
model, ok := result.(Model)
if !ok {
t.Fatal("Update did not return a Model")
}
return model
}
func TestModel_View_NotReady(t *testing.T) {
m := newTestModel()
view := m.View()
if !strings.Contains(view, "Initializing...") {
t.Errorf("expected 'Initializing...' in view, got %q", view)
}
}
func TestModel_View_Quitting(t *testing.T) {
m := newTestModel()
m.quitting = true
view := m.View()
if !strings.Contains(view, "Goodbye!") {
t.Errorf("expected 'Goodbye!' in view, got %q", view)
}
}
func TestModel_Update_CtrlC_SetsQuitting(t *testing.T) {
m := initTestModel(t)
result, cmd := m.Update(tea.KeyMsg{Type: tea.KeyCtrlC})
model, ok := result.(Model)
if !ok {
t.Fatal("Update did not return a Model")
}
if !model.quitting {
t.Error("expected quitting to be true after Ctrl+C")
}
if cmd == nil {
t.Error("expected quit command, got nil")
}
}
func TestModel_Update_ResponseMsg_AddsAssistantMessage(t *testing.T) {
m := initTestModel(t)
m.thinking = true
result, _ := m.Update(ResponseMsg{Content: "Hello, world!"})
model, ok := result.(Model)
if !ok {
t.Fatal("Update did not return a Model")
}
if model.thinking {
t.Error("expected thinking to be false after ResponseMsg")
}
found := false
for _, msg := range model.messages {
if msg.role == roleAssistant && msg.content == "Hello, world!" {
found = true
break
}
}
if !found {
t.Error("expected assistant message 'Hello, world!' in messages")
}
}
func TestModel_Update_ToolCallStarted_AddsToolMessage(t *testing.T) {
m := initTestModel(t)
result, _ := m.Update(ToolCallStartedMsg{
ID: "tool-1",
Name: "web_search",
Args: `{"query":"test"}`,
})
model, ok := result.(Model)
if !ok {
t.Fatal("Update did not return a Model")
}
if len(model.messages) != 1 {
t.Fatalf("expected 1 message, got %d", len(model.messages))
}
msg := model.messages[0]
if msg.role != roleTool {
t.Errorf("expected role 'tool', got %q", msg.role)
}
if msg.toolName != "web_search" {
t.Errorf("expected toolName 'web_search', got %q", msg.toolName)
}
if msg.toolID != "tool-1" {
t.Errorf("expected toolID 'tool-1', got %q", msg.toolID)
}
if msg.toolDone {
t.Error("expected toolDone to be false")
}
}
func TestModel_Update_ToolCallCompleted_UpdatesToolMessage(t *testing.T) {
m := initTestModel(t)
// First start a tool
result, _ := m.Update(ToolCallStartedMsg{
ID: "tool-1",
Name: "web_search",
})
m = result.(Model)
// Then complete it
result, _ = m.Update(ToolCallCompletedMsg{
ID: "tool-1",
Name: "web_search",
})
model := result.(Model)
if len(model.messages) != 1 {
t.Fatalf("expected 1 message, got %d", len(model.messages))
}
msg := model.messages[0]
if !msg.toolDone {
t.Error("expected toolDone to be true after completion")
}
if msg.toolErr {
t.Error("expected toolErr to be false for successful completion")
}
if !strings.Contains(msg.content, "done") {
t.Errorf("expected content to contain 'done', got %q", msg.content)
}
}
func TestModel_Update_ToolCallCompleted_Error(t *testing.T) {
m := initTestModel(t)
result, _ := m.Update(ToolCallStartedMsg{
ID: "tool-2",
Name: "exec",
})
m = result.(Model)
result, _ = m.Update(ToolCallCompletedMsg{
ID: "tool-2",
Name: "exec",
IsError: true,
})
model := result.(Model)
msg := model.messages[0]
if !msg.toolErr {
t.Error("expected toolErr to be true for error completion")
}
if !strings.Contains(msg.content, "failed") {
t.Errorf("expected content to contain 'failed', got %q", msg.content)
}
}
func TestModel_Update_ErrorMsg_AddsSystemMessage(t *testing.T) {
m := initTestModel(t)
m.thinking = true
result, _ := m.Update(ErrorMsg{Err: fmt.Errorf("connection refused")})
model, ok := result.(Model)
if !ok {
t.Fatal("Update did not return a Model")
}
if model.thinking {
t.Error("expected thinking to be false after ErrorMsg")
}
found := false
for _, msg := range model.messages {
if msg.role == roleSystem && strings.Contains(msg.content, "connection refused") {
found = true
break
}
}
if !found {
t.Error("expected system message containing 'connection refused'")
}
}
func TestModel_Update_SlashCommandResult(t *testing.T) {
m := initTestModel(t)
result, _ := m.Update(SlashCommandResultMsg{Result: "Session: default"})
model := result.(Model)
found := false
for _, msg := range model.messages {
if msg.role == roleSystem && msg.content == "Session: default" {
found = true
break
}
}
if !found {
t.Error("expected system message with slash command result")
}
}
func TestModel_Update_WindowSizeMsg_SetsReady(t *testing.T) {
m := newTestModel()
if m.ready {
t.Fatal("expected model to not be ready initially")
}
result, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40})
model := result.(Model)
if !model.ready {
t.Error("expected model to be ready after WindowSizeMsg")
}
if model.width != 120 {
t.Errorf("expected width 120, got %d", model.width)
}
if model.height != 40 {
t.Errorf("expected height 40, got %d", model.height)
}
}