//go:build integration // +build integration package integration import ( "context" "os" "path/filepath" "strings" "testing" "time" "ai-agent/internal/agent" "ai-agent/internal/command" "ai-agent/internal/config" "ai-agent/internal/llm" "ai-agent/internal/mcp" "ai-agent/internal/tui" tea "charm.land/bubbletea/v2" ) func skipIfNoOllama(t *testing.T) { if os.Getenv("OLLAMA_HOST") == "" { os.Setenv("OLLAMA_HOST", "http://localhost:11434") } client := llm.NewClient(llm.Config{ BaseURL: os.Getenv("OLLAMA_HOST"), Model: "qwen3.5:2b", NumCtx: 262144, }) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() if err := client.Ping(ctx); err != nil { t.Skip("Ollama not available: skipping integration test") } } func TestTUI_Initialization(t *testing.T) { skipIfNoOllama(t) reg := command.NewRegistry() command.RegisterBuiltins(reg) cfg := config.DefaultModelConfig() router := config.NewRouter(&cfg) modelManager := llm.NewModelManager("http://localhost:11434", 262144) modelManager.SetCurrentModel("qwen3.5:2b") ag := agent.New(modelManager, mcp.NewRegistry(), cfg.Ollama.NumCtx) ag.SetRouter(router) completer := tui.NewCompleter(reg, []string{"qwen3.5:2b"}, nil, nil, nil) m := tui.New(ag, reg, nil, completer, modelManager, router, nil) updated, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40}) m = updated.(*tui.Model) if !m.Ready() { t.Error("TUI should be ready after WindowSizeMsg") } } func TestTUI_ScrollAnchorDuringStreaming(t *testing.T) { skipIfNoOllama(t) reg := command.NewRegistry() command.RegisterBuiltins(reg) cfg := config.DefaultModelConfig() router := config.NewRouter(&cfg) modelManager := llm.NewModelManager("http://localhost:11434", 262144) ag := agent.New(modelManager, mcp.NewRegistry(), 262144) ag.SetRouter(router) completer := tui.NewCompleter(reg, []string{"qwen3.5:2b"}, nil, nil, nil) m := tui.New(ag, reg, nil, completer, modelManager, router, nil) updated, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40}) m = updated.(*tui.Model) if !m.AnchorActive() { t.Error("anchorActive should be true after initialization") } updated, _ = m.Update(tui.StreamTextMsg{Text: "Hello"}) m = updated.(*tui.Model) if !m.AnchorActive() { t.Error("anchorActive should remain true during streaming") } } func TestTUI_OverlayRendering(t *testing.T) { reg := command.NewRegistry() command.RegisterBuiltins(reg) cfg := config.DefaultModelConfig() router := config.NewRouter(&cfg) modelManager := llm.NewModelManager("http://localhost:11434", 262144) ag := agent.New(modelManager, mcp.NewRegistry(), 262144) ag.SetRouter(router) completer := tui.NewCompleter(reg, []string{"qwen3.5:2b"}, nil, nil, nil) m := tui.New(ag, reg, nil, completer, modelManager, router, nil) updated, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40}) m = updated.(*tui.Model) updated, _ = m.Update(tui.KeyPressMsg{Code: '?'}) m = updated.(*tui.Model) view := m.View() if view == nil { t.Error("View should not be nil") } updated, _ = m.Update(tui.KeyPressMsg{Code: tea.KeyEscape}) m = updated.(*tui.Model) } func TestTUI_ToolCardRendering(t *testing.T) { reg := command.NewRegistry() command.RegisterBuiltins(reg) cfg := config.DefaultModelConfig() router := config.NewRouter(&cfg) modelManager := llm.NewModelManager("http://localhost:11434", 262144) ag := agent.New(modelManager, mcp.NewRegistry(), 262144) ag.SetRouter(router) completer := tui.NewCompleter(reg, []string{"qwen3.5:2b"}, nil, nil, nil) m := tui.New(ag, reg, nil, completer, modelManager, router, nil) updated, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40}) m = updated.(*tui.Model) startTime := time.Now() updated, _ = m.Update(tui.ToolCallStartMsg{ Name: "read_file", Args: map[string]any{"path": "test.go"}, StartTime: startTime, }) m = updated.(*tui.Model) updated, _ = m.Update(tui.ToolCallResultMsg{ Name: "read_file", Result: "file content", IsError: false, Duration: 100 * time.Millisecond, }) m = updated.(*tui.Model) view := m.View() if view == nil { t.Error("View should not be nil after tool execution") } } func TestQwenRouter_Integration(t *testing.T) { cfg := config.DefaultModelConfig() router := config.NewQwenModelRouter(&cfg) tests := []struct { query string mode config.ModeContext expectSmaller string expectLarger string }{ {"what is go?", config.ModeAskContext, "qwen3.5:2b", ""}, {"design architecture", config.ModeBuildContext, "", "qwen3.5:4b"}, {"plan the system", config.ModePlanContext, "", "qwen3.5:4b"}, } for _, tt := range tests { t.Run(tt.query, func(t *testing.T) { model := router.SelectModelForMode(tt.query, tt.mode) if tt.expectSmaller != "" { if modelRank(model) > modelRank(tt.expectSmaller) { t.Errorf("model %s is larger than expected %s", model, tt.expectSmaller) } } if tt.expectLarger != "" { if modelRank(model) < modelRank(tt.expectLarger) { t.Errorf("model %s is smaller than expected %s", model, tt.expectLarger) } } }) } } func modelRank(model string) int { switch { case strings.Contains(model, "0.8b"): return 1 case strings.Contains(model, "2b"): return 2 case strings.Contains(model, "4b"): return 3 case strings.Contains(model, "9b"): return 4 default: return 0 } } func TestFileOperations_Integration(t *testing.T) { skipIfNoOllama(t) tmpDir := t.TempDir() testFile := filepath.Join(tmpDir, "test.txt") if err := os.WriteFile(testFile, []byte("hello world"), 0644); err != nil { t.Fatalf("failed to create test file: %v", err) } content, err := os.ReadFile(testFile) if err != nil { t.Fatalf("failed to read test file: %v", err) } if string(content) != "hello world" { t.Errorf("unexpected file content: %q", string(content)) } } func BenchmarkTUI_Render(b *testing.B) { reg := command.NewRegistry() command.RegisterBuiltins(reg) cfg := config.DefaultModelConfig() router := config.NewRouter(&cfg) modelManager := llm.NewModelManager("http://localhost:11434", 262144) ag := agent.New(modelManager, mcp.NewRegistry(), 262144) ag.SetRouter(router) completer := tui.NewCompleter(reg, []string{"qwen3.5:2b"}, nil, nil, nil) m := tui.New(ag, reg, nil, completer, modelManager, router, nil) updated, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 40}) m = updated.(*tui.Model) b.ResetTimer() for i := 0; i < b.N; i++ { _ = m.View() } } func BenchmarkQwenRouter_Classification(b *testing.B) { cfg := config.DefaultModelConfig() router := config.NewQwenModelRouter(&cfg) queries := []string{ "what is go", "how do i create a file", "debug this nil pointer error", "design microservice architecture", } b.ResetTimer() for i := 0; i < b.N; i++ { for _, q := range queries { _ = router.SelectModelForMode(q, config.ModeAskContext) } } }