v0.0.1: agentu mvp
Add OpenAI-compatible providers, TUI, slash commands, and model switching. Add local file tools plus optional yolo shell execution. Document config, usage, and development workflow.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func TestModelRendersChatShell(t *testing.T) {
|
||||
m := newModel(context.Background(), nil, Options{ModelName: "test-model", Yolo: true})
|
||||
next, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 30})
|
||||
rendered := next.(model).View()
|
||||
|
||||
for _, want := range []string{"agentu", "test-model", "yolo tools", "light", "ctrl+j", "alt+enter"} {
|
||||
if !strings.Contains(rendered, want) {
|
||||
t.Fatalf("rendered view missing %q:\n%s", want, rendered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCtrlJInsertsInputNewline(t *testing.T) {
|
||||
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
|
||||
m.input.SetValue("hello")
|
||||
|
||||
next, _ := m.Update(tea.KeyMsg{Type: tea.KeyCtrlJ})
|
||||
got := next.(model).input.Value()
|
||||
if got != "hello\n" {
|
||||
t.Fatalf("input value = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInputStartsAtOneLineAndGrows(t *testing.T) {
|
||||
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
|
||||
if got := m.input.Height(); got != 1 {
|
||||
t.Fatalf("initial input height = %d", got)
|
||||
}
|
||||
|
||||
m.input.SetValue("one\ntwo\nthree")
|
||||
m.afterInputChanged()
|
||||
if got := m.input.Height(); got != 3 {
|
||||
t.Fatalf("multiline input height = %d", got)
|
||||
}
|
||||
|
||||
m.input.SetValue(strings.Repeat("line\n", 20))
|
||||
m.afterInputChanged()
|
||||
if got := m.input.Height(); got != maxInputLines {
|
||||
t.Fatalf("clamped input height = %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlashCommandSuggestions(t *testing.T) {
|
||||
m := newModel(context.Background(), nil, Options{ModelName: "test-model", Yolo: true})
|
||||
next, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 30})
|
||||
m = next.(model)
|
||||
m.input.SetValue("/")
|
||||
m.afterInputChanged()
|
||||
|
||||
rendered := m.View()
|
||||
for _, want := range []string{"/clear reset context", "/exit quit", "/model model/provider/thinking"} {
|
||||
if !strings.Contains(rendered, want) {
|
||||
t.Fatalf("rendered suggestions missing %q:\n%s", want, rendered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelCommand(t *testing.T) {
|
||||
m := newModel(context.Background(), nil, Options{ModelName: "test-model", Yolo: true})
|
||||
m.input.SetValue("/model")
|
||||
|
||||
next, _ := m.submit()
|
||||
rendered := next.(model).renderMessages()
|
||||
for _, want := range []string{"model: test-model", "mode: yolo tools", "theme: light"} {
|
||||
if !strings.Contains(rendered, want) {
|
||||
t.Fatalf("model command output missing %q:\n%s", want, rendered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelThinkingCommand(t *testing.T) {
|
||||
manager := &fakeModelManager{model: "test-model", thinking: "none"}
|
||||
m := newModel(context.Background(), nil, Options{ModelName: "test-model", ModelManager: manager})
|
||||
m.input.SetValue("/model thinking high")
|
||||
|
||||
next, _ := m.submit()
|
||||
rendered := next.(model).renderMessages()
|
||||
if manager.thinking != "high" {
|
||||
t.Fatalf("thinking = %q", manager.thinking)
|
||||
}
|
||||
if !strings.Contains(rendered, "thinking: high") {
|
||||
t.Fatalf("model command output missing thinking:\n%s", rendered)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeModelManager struct {
|
||||
provider string
|
||||
model string
|
||||
thinking string
|
||||
}
|
||||
|
||||
func (f *fakeModelManager) CurrentProvider() string {
|
||||
if f.provider == "" {
|
||||
return "default"
|
||||
}
|
||||
return f.provider
|
||||
}
|
||||
|
||||
func (f *fakeModelManager) CurrentModel() string {
|
||||
return f.model
|
||||
}
|
||||
|
||||
func (f *fakeModelManager) CurrentThinking() string {
|
||||
return f.thinking
|
||||
}
|
||||
|
||||
func (f *fakeModelManager) Info() string {
|
||||
return "provider: " + f.CurrentProvider() + "\nmodel: " + f.model + "\nthinking: " + f.thinking
|
||||
}
|
||||
|
||||
func (f *fakeModelManager) SetProvider(name string) (string, error) {
|
||||
f.provider = name
|
||||
return f.Info(), nil
|
||||
}
|
||||
|
||||
func (f *fakeModelManager) SetModel(model string) (string, error) {
|
||||
f.model = model
|
||||
return f.Info(), nil
|
||||
}
|
||||
|
||||
func (f *fakeModelManager) SetThinking(level string) (string, error) {
|
||||
f.thinking = level
|
||||
return f.Info(), nil
|
||||
}
|
||||
|
||||
func TestParseThemeMode(t *testing.T) {
|
||||
for input, want := range map[string]ThemeMode{
|
||||
"": ThemeLight,
|
||||
"light": ThemeLight,
|
||||
"dark": ThemeDark,
|
||||
"DARK": ThemeDark,
|
||||
} {
|
||||
got, err := ParseThemeMode(input)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseThemeMode(%q): %v", input, err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("ParseThemeMode(%q) = %q, want %q", input, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := ParseThemeMode("sepia"); err == nil {
|
||||
t.Fatal("expected invalid theme error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanToolLog(t *testing.T) {
|
||||
got := cleanToolLog("\n[tool] shell_run {\"command\":\"pwd\"}\n")
|
||||
want := `shell_run {"command":"pwd"}`
|
||||
if got != want {
|
||||
t.Fatalf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user