7b8bad5e62
TUI improvements: - semantic icon constants (✓ ◆ ▣ ✕ ⌘ ◌ ◔ ◉) - pill-style metadata bar with model/provider/session/context - fuzzy command palette with deterministic subsequence scoring - input history navigation (ctrl+p / ctrl+n) - structured tool log cards (▣ tool_name + args preview) - no-background activity indicator and meta bar for cleaner rendering Startup behavior: - default startup creates a fresh session - explicit --resume <id> restores saved context - added main_test.go for initializeSession coverage
577 lines
18 KiB
Go
577 lines
18 KiB
Go
package tui
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"agentu/internal/agent"
|
|
"agentu/pkg/llm"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
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})
|
|
m = next.(model)
|
|
rendered := m.View()
|
|
|
|
for _, want := range []string{"test-model", "yolo tools", "light", "ctrl+j", "alt+enter", "ctrl+p/ctrl+n history"} {
|
|
if !strings.Contains(rendered, want) {
|
|
t.Fatalf("rendered view missing %q:\n%s", want, rendered)
|
|
}
|
|
}
|
|
if strings.Contains(rendered, "local agent") {
|
|
t.Fatalf("top header should not render:\n%s", rendered)
|
|
}
|
|
if _, ok := m.styles.App.GetBackground().(lipgloss.NoColor); !ok {
|
|
t.Fatalf("app should not paint a global background")
|
|
}
|
|
if _, ok := m.styles.Viewport.GetBackground().(lipgloss.NoColor); !ok {
|
|
t.Fatalf("viewport should not paint an assistant background")
|
|
}
|
|
}
|
|
|
|
func TestInputTextDoesNotPaintBackground(t *testing.T) {
|
|
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
|
|
for name, color := range map[string]any{
|
|
"input box": m.styles.InputBox.GetBackground(),
|
|
"focused base": m.input.FocusedStyle.Base.GetBackground(),
|
|
"focused cursor line": m.input.FocusedStyle.CursorLine.GetBackground(),
|
|
"blurred base": m.input.BlurredStyle.Base.GetBackground(),
|
|
"blurred cursor line": m.input.BlurredStyle.CursorLine.GetBackground(),
|
|
} {
|
|
if _, ok := color.(lipgloss.NoColor); !ok {
|
|
t.Fatalf("%s should not paint input text background", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInputBoxIsRoomier(t *testing.T) {
|
|
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
|
|
next, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 30})
|
|
m = next.(model)
|
|
|
|
if got := m.styles.InputBox.GetVerticalFrameSize(); got != 2 {
|
|
t.Fatalf("input box vertical frame should stay compact, got %d", got)
|
|
}
|
|
if got := m.styles.InputBox.GetHorizontalFrameSize(); got < 6 {
|
|
t.Fatalf("input box horizontal frame should include roomier padding, got %d", got)
|
|
}
|
|
if got, want := m.input.Width(), 100-m.styles.InputBox.GetHorizontalFrameSize(); got != want {
|
|
t.Fatalf("input content width = %d, want %d", got, want)
|
|
}
|
|
rendered := m.inputView()
|
|
line := strings.Split(rendered, "\n")[0]
|
|
if got := lipgloss.Width(line); got != 100 {
|
|
t.Fatalf("input box rendered width = %d, want 100:\n%s", got, rendered)
|
|
}
|
|
}
|
|
|
|
func TestModelMetaRendersBelowInput(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()
|
|
|
|
inputIndex := strings.Index(rendered, "Message agentu")
|
|
modelIndex := strings.Index(rendered, "test-model")
|
|
if inputIndex < 0 || modelIndex < 0 {
|
|
t.Fatalf("rendered view missing input or model meta:\n%s", rendered)
|
|
}
|
|
if modelIndex < inputIndex {
|
|
t.Fatalf("model meta should render below input:\n%s", rendered)
|
|
}
|
|
}
|
|
|
|
func TestModelMetaRendersContextUsage(t *testing.T) {
|
|
assistant := agent.New(agent.Options{Model: "test-model", MaxContextTokens: 1000})
|
|
assistant.SetMessages([]llm.Message{{Role: llm.RoleUser, Content: strings.Repeat("x", 800)}})
|
|
m := newModel(context.Background(), assistant, Options{ModelName: "test-model"})
|
|
next, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 30})
|
|
rendered := next.(model).modelMetaView()
|
|
|
|
for _, want := range []string{"ctx ~", "/1k", "%"} {
|
|
if !strings.Contains(rendered, want) {
|
|
t.Fatalf("model meta missing %q:\n%s", want, rendered)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInitialViewDoesNotRenderTopNote(t *testing.T) {
|
|
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
|
|
next, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 30})
|
|
rendered := next.(model).View()
|
|
|
|
for _, unwanted := range []string{"Note", "Ask anything", "Start a conversation"} {
|
|
if strings.Contains(rendered, unwanted) {
|
|
t.Fatalf("initial view should not render %q:\n%s", unwanted, rendered)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunningStateRendersActivityIndicator(t *testing.T) {
|
|
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
|
|
next, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 30})
|
|
m = next.(model)
|
|
m.running = true
|
|
m.status = "thinking..."
|
|
m.resize()
|
|
|
|
rendered := m.View()
|
|
for _, want := range []string{iconWorking, "Working", "thinking...", "esc cancels"} {
|
|
if !strings.Contains(rendered, want) {
|
|
t.Fatalf("running view missing %q:\n%s", want, rendered)
|
|
}
|
|
}
|
|
if backgroundPattern.MatchString(m.activityView()) {
|
|
t.Fatalf("activity view should not paint a background:\n%q", m.activityView())
|
|
}
|
|
}
|
|
|
|
func TestUserMessageRendersFullWidthBubble(t *testing.T) {
|
|
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
|
|
next, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 30})
|
|
m = next.(model)
|
|
|
|
rendered := m.messageView(roleUser, "hi", 80)
|
|
if !strings.Contains(rendered, "hi") {
|
|
t.Fatalf("user message missing content:\n%s", rendered)
|
|
}
|
|
if strings.Contains(rendered, "You") {
|
|
t.Fatalf("user message should not render label:\n%s", rendered)
|
|
}
|
|
if !strings.Contains(rendered, " hi") || !strings.Contains(rendered, "hi ") {
|
|
t.Fatalf("user message should have horizontal padding:\n%q", rendered)
|
|
}
|
|
lines := strings.Split(rendered, "\n")
|
|
if len(lines) < 3 {
|
|
t.Fatalf("user message background should include vertical padding:\n%q", rendered)
|
|
}
|
|
for _, line := range lines {
|
|
if got := lipgloss.Width(line); got != 80 {
|
|
t.Fatalf("user message background should span full width, line width = %d:\n%q", got, rendered)
|
|
}
|
|
}
|
|
m.width = 80
|
|
m.messages = []message{{role: roleUser, content: "hi"}}
|
|
rendered = m.renderMessages()
|
|
for _, line := range strings.Split(rendered, "\n") {
|
|
if got := lipgloss.Width(line); got != 80 {
|
|
t.Fatalf("rendered user transcript should span full width, line width = %d:\n%q", got, rendered)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAssistantMessagesRenderMarkdownWithoutChrome(t *testing.T) {
|
|
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
|
|
next, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 30})
|
|
m = next.(model)
|
|
|
|
rendered := m.messageView(roleAssistant, "# Title\n\n- item", 80)
|
|
for _, want := range []string{"Title", "item"} {
|
|
if !strings.Contains(rendered, want) {
|
|
t.Fatalf("assistant markdown missing %q:\n%s", want, rendered)
|
|
}
|
|
}
|
|
for _, unwanted := range []string{"Agentu", "│", "┃", "|"} {
|
|
if strings.Contains(rendered, unwanted) {
|
|
t.Fatalf("assistant message should not render chrome %q:\n%s", unwanted, rendered)
|
|
}
|
|
}
|
|
if strings.Contains(rendered, "# Title") {
|
|
t.Fatalf("assistant markdown heading was not rendered:\n%s", rendered)
|
|
}
|
|
}
|
|
|
|
func TestAssistantMessageRendersPaddedBodyWithoutBackground(t *testing.T) {
|
|
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
|
|
next, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 30})
|
|
m = next.(model)
|
|
if _, ok := m.styles.AssistantMsg.GetBackground().(lipgloss.NoColor); !ok {
|
|
t.Fatalf("assistant message should not set a background color")
|
|
}
|
|
|
|
rendered := m.messageView(roleAssistant, "hello", 80)
|
|
if !strings.Contains(rendered, "hello") {
|
|
t.Fatalf("assistant message missing content:\n%s", rendered)
|
|
}
|
|
lines := strings.Split(rendered, "\n")
|
|
if len(lines) < 3 {
|
|
t.Fatalf("assistant message should include vertical padding:\n%q", rendered)
|
|
}
|
|
if got := lipgloss.Width(rendered); got <= len("hello") {
|
|
t.Fatalf("assistant message should be wider than text, width = %d:\n%q", got, rendered)
|
|
}
|
|
for _, line := range lines {
|
|
if got := lipgloss.Width(line); got >= 80 {
|
|
t.Fatalf("assistant message should not fill the row, line width = %d:\n%q", got, rendered)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUserMessagesDoNotRenderMarkdown(t *testing.T) {
|
|
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
|
|
next, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 30})
|
|
m = next.(model)
|
|
|
|
rendered := m.messageView(roleUser, "# Title\n\n- item", 80)
|
|
for _, want := range []string{"# Title", "- item"} {
|
|
if !strings.Contains(rendered, want) {
|
|
t.Fatalf("user markdown should stay plain and include %q:\n%s", want, rendered)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMessageRoleLabelsDoNotLabelUserOrAssistant(t *testing.T) {
|
|
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
|
|
next, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 30})
|
|
m = next.(model)
|
|
m.messages = []message{
|
|
{role: roleUser, content: "hello"},
|
|
{role: roleAssistant, content: "hi"},
|
|
}
|
|
|
|
rendered := m.renderMessages()
|
|
for _, want := range []string{"hello", "hi"} {
|
|
if !strings.Contains(rendered, want) {
|
|
t.Fatalf("messages missing content %q:\n%s", want, rendered)
|
|
}
|
|
}
|
|
for _, unwanted := range []string{"You", "Agentu"} {
|
|
if strings.Contains(rendered, unwanted) {
|
|
t.Fatalf("message should not render label %q:\n%s", unwanted, 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 TestCommandPaletteFuzzyMatches(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("/mdl")
|
|
m.afterInputChanged()
|
|
|
|
plain := stripANSI(m.View())
|
|
for _, want := range []string{iconCommand + " Commands", "/model model/provider/thinking"} {
|
|
if !strings.Contains(plain, want) {
|
|
t.Fatalf("rendered palette missing %q:\n%s", want, plain)
|
|
}
|
|
}
|
|
|
|
matches := commandMatches("m")
|
|
modelIndex := -1
|
|
renameIndex := -1
|
|
for i, match := range matches {
|
|
switch match.command.Name {
|
|
case "/model":
|
|
modelIndex = i
|
|
case "/rename":
|
|
renameIndex = i
|
|
}
|
|
}
|
|
if modelIndex < 0 || renameIndex < 0 || modelIndex > renameIndex {
|
|
t.Fatalf("/model should sort before /rename for /m query: %#v", matches)
|
|
}
|
|
}
|
|
|
|
func TestInputHistoryNavigation(t *testing.T) {
|
|
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
|
|
m.rememberInput("first")
|
|
m.rememberInput("second")
|
|
|
|
next, _ := m.Update(tea.KeyMsg{Type: tea.KeyCtrlP})
|
|
m = next.(model)
|
|
if got := m.input.Value(); got != "second" {
|
|
t.Fatalf("after ctrl+p input = %q", got)
|
|
}
|
|
next, _ = m.Update(tea.KeyMsg{Type: tea.KeyCtrlP})
|
|
m = next.(model)
|
|
if got := m.input.Value(); got != "first" {
|
|
t.Fatalf("after second ctrl+p input = %q", got)
|
|
}
|
|
next, _ = m.Update(tea.KeyMsg{Type: tea.KeyCtrlN})
|
|
m = next.(model)
|
|
if got := m.input.Value(); got != "second" {
|
|
t.Fatalf("after ctrl+n input = %q", got)
|
|
}
|
|
next, _ = m.Update(tea.KeyMsg{Type: tea.KeyCtrlN})
|
|
m = next.(model)
|
|
if got := m.input.Value(); got != "" {
|
|
t.Fatalf("after second ctrl+n input = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestToolMessageRendersStructuredCard(t *testing.T) {
|
|
m := newModel(context.Background(), nil, Options{ModelName: "test-model"})
|
|
m.messages = append(m.messages, message{role: roleTool, content: `file_read {"path":"README.md"}`})
|
|
|
|
plain := stripANSI(m.renderMessages())
|
|
for _, want := range []string{iconTool + " file_read", "README.md"} {
|
|
if !strings.Contains(plain, want) {
|
|
t.Fatalf("tool card missing %q:\n%s", want, plain)
|
|
}
|
|
}
|
|
}
|
|
|
|
type tuiCompactProvider struct{}
|
|
|
|
func (tuiCompactProvider) ChatStream(ctx context.Context, req llm.ChatRequest, emit func(llm.StreamEvent) error) error {
|
|
return emit(llm.StreamEvent{Content: "summary from tui"})
|
|
}
|
|
|
|
func TestCompactCommand(t *testing.T) {
|
|
assistant := agent.New(agent.Options{Provider: tuiCompactProvider{}, Model: "test", SystemPrompt: "system"})
|
|
assistant.SetMessages([]llm.Message{
|
|
{Role: llm.RoleSystem, Content: "system"},
|
|
{Role: llm.RoleUser, Content: "old question 1"},
|
|
{Role: llm.RoleAssistant, Content: "old answer 1"},
|
|
{Role: llm.RoleUser, Content: "old question 2"},
|
|
{Role: llm.RoleAssistant, Content: "old answer 2"},
|
|
{Role: llm.RoleUser, Content: "recent question 1"},
|
|
{Role: llm.RoleAssistant, Content: "recent answer 1"},
|
|
{Role: llm.RoleUser, Content: "recent question 2"},
|
|
{Role: llm.RoleAssistant, Content: "recent answer 2"},
|
|
})
|
|
m := newModel(context.Background(), assistant, Options{ModelName: "test-model"})
|
|
|
|
out, err := m.handleLocalCommand("/compact")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out != "Context compacted." {
|
|
t.Fatalf("output = %q", out)
|
|
}
|
|
if !strings.Contains(messageContents(assistant.Messages()), "[context summary]") {
|
|
t.Fatalf("messages missing summary: %#v", assistant.Messages())
|
|
}
|
|
}
|
|
|
|
func messageContents(messages []llm.Message) string {
|
|
var b strings.Builder
|
|
for _, msg := range messages {
|
|
b.WriteString(msg.Content)
|
|
b.WriteByte('\n')
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func TestWorkflowStatusDiffAndTestCommands(t *testing.T) {
|
|
if _, err := exec.LookPath("git"); err != nil {
|
|
t.Skip("git not installed")
|
|
}
|
|
dir := t.TempDir()
|
|
runInDir(t, dir, "git", "init")
|
|
runInDir(t, dir, "git", "config", "user.email", "agentu@example.test")
|
|
runInDir(t, dir, "git", "config", "user.name", "agentu")
|
|
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/p1\n\ngo 1.26\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "main_test.go"), []byte("package p1\n\nimport \"testing\"\n\nfunc TestOK(t *testing.T) {}\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runInDir(t, dir, "git", "add", ".")
|
|
runInDir(t, dir, "git", "commit", "-m", "init")
|
|
if err := os.WriteFile(filepath.Join(dir, "main_test.go"), []byte("package p1\n\nimport \"testing\"\n\nfunc TestOK(t *testing.T) { t.Log(\"changed\") }\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
m := newModel(context.Background(), nil, Options{ModelName: "test-model", WorkingDir: dir})
|
|
status, err := m.handleStatusCommand(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(status, "main_test.go") {
|
|
t.Fatalf("status missing changed file:\n%s", status)
|
|
}
|
|
diff, err := m.handleDiffCommand(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(diff, "changed") {
|
|
t.Fatalf("diff missing changed content:\n%s", diff)
|
|
}
|
|
out, err := m.handleTestCommand(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out, "$ go test ./...") {
|
|
t.Fatalf("test output missing default command:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func runInDir(t *testing.T, dir string, name string, args ...string) {
|
|
t.Helper()
|
|
cmd := exec.Command(name, args...)
|
|
cmd.Dir = dir
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("%s %v failed: %v\n%s", name, args, err, output)
|
|
}
|
|
}
|
|
|
|
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
|
|
sessionName string
|
|
saved bool
|
|
}
|
|
|
|
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) CurrentSessionID() string {
|
|
return "test-id"
|
|
}
|
|
|
|
func (f *fakeModelManager) CurrentSessionName() string {
|
|
if f.sessionName == "" {
|
|
return "test-session"
|
|
}
|
|
return f.sessionName
|
|
}
|
|
|
|
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 (f *fakeModelManager) Save() error {
|
|
f.saved = true
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeModelManager) Resume(id string) error {
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeModelManager) Rename(name string) (string, error) {
|
|
f.sessionName = name
|
|
return "Renamed.", nil
|
|
}
|
|
|
|
func (f *fakeModelManager) Sessions() (string, error) {
|
|
return "No sessions.", nil
|
|
}
|
|
|
|
func (f *fakeModelManager) NewSession() (string, error) {
|
|
return "New session.", 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)
|
|
}
|
|
}
|