Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 0 additions & 149 deletions internal/cli/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,85 +273,6 @@ func (m *TestUIModel) executeTool(toolCall agent.ToolCall) tea.Cmd {
}
}

if toolCall.Name == agent.ToolExecuteTest {
method, _ := toolCall.Arguments["method"].(string)
endpoint, _ := toolCall.Arguments["endpoint"].(string)

if method == "" || endpoint == "" {
return toolResultMsg{
toolID: toolCall.ID,
toolName: toolCall.Name,
result: nil,
err: fmt.Errorf("missing required parameters: method and endpoint"),
}
}

expectedStatus := 200
if es, ok := toolCall.Arguments["expected_status"].(float64); ok {
expectedStatus = int(es)
} else if es, ok := toolCall.Arguments["expected_status"].(int); ok {
expectedStatus = es
}

headers := make(map[string]string)
if h, ok := toolCall.Arguments["headers"].(map[string]any); ok {
for k, v := range h {
if vs, ok := v.(string); ok {
headers[k] = vs
}
}
}

var body any
if b, ok := toolCall.Arguments["body"]; ok {
body = b
}

requiresAuth := false
if ra, ok := toolCall.Arguments["requires_auth"].(bool); ok {
requiresAuth = ra
}

result, err := m.testExecutor.ExecuteTest(method, endpoint, headers, body, requiresAuth)

if err != nil {
return toolResultMsg{
toolID: toolCall.ID,
toolName: toolCall.Name,
result: map[string]any{
"method": method,
"endpoint": endpoint,
"error": err.Error(),
"expected_status": expectedStatus,
"passed": false,
},
err: err,
}
}

passed := result.StatusCode == expectedStatus

schemaErrors := m.validateResponseSchema(method, endpoint, result.StatusCode, result.ResponseBody)

return toolResultMsg{
toolID: toolCall.ID,
toolName: toolCall.Name,
result: map[string]any{
"method": method,
"endpoint": endpoint,
"status_code": result.StatusCode,
"expected_status": expectedStatus,
"response_body": result.ResponseBody,
"headers": result.Headers,
"duration_ms": result.Duration.Milliseconds(),
"passed": passed,
"schema_valid": len(schemaErrors) == 0,
"schema_errors": schemaErrors,
},
err: nil,
}
}

if toolCall.Name == agent.ToolGenerateReport {
reportContent, _ := toolCall.Arguments["report_content"].(string)
if reportContent == "" {
Expand Down Expand Up @@ -471,76 +392,6 @@ func (m *TestUIModel) executeTool(toolCall agent.ToolCall) tea.Cmd {
}

func (m *TestUIModel) handleToolResult(toolName string, toolID string, result any) tea.Cmd {
if toolName == agent.ToolExecuteTest {
if resultMap, ok := result.(map[string]any); ok {
method, _ := resultMap["method"].(string)
endpoint, _ := resultMap["endpoint"].(string)
statusCode, _ := resultMap["status_code"].(int)
expectedStatus, _ := resultMap["expected_status"].(int)
responseBody, _ := resultMap["response_body"].(string)
durationMs, _ := resultMap["duration_ms"].(int64)

var passed bool
if p, ok := resultMap["passed"].(bool); ok {
passed = p
} else {
passed = statusCode == expectedStatus
}

methodStyle, ok := m.methodStyles[method]
if !ok {
methodStyle = lipgloss.NewStyle().Foreground(Theme.TextSubtle)
}
methodFormatted := methodStyle.Render(method)

statusStyle := m.successStyle
statusIcon := "✓"
if !passed {
statusStyle = m.errorStyle
statusIcon = "✗"
if m.isHeadless {
m.headlessExitCode = 1
}
}

statusMsg := fmt.Sprintf(" Status: %d", statusCode)
if !passed && expectedStatus > 0 {
statusMsg += fmt.Sprintf(" (expected %d)", expectedStatus)
}
statusMsg += fmt.Sprintf(" | Duration: %dms", durationMs)

m.addMessage("")
m.addMessage(statusStyle.Render(statusIcon) + " " + methodFormatted + " " + endpoint)
m.addMessage(m.subtleStyle.Render(statusMsg))

if len(responseBody) > 0 {
preview := responseBody
if len(preview) > 200 {
preview = preview[:200] + "..."
}
m.addMessage(m.subtleStyle.Render(" Response: " + preview))
}

if toolID != "" {
chatMsg := agent.ChatMessage{
Role: "user",
FunctionResponse: &agent.FunctionResponseData{
ID: toolID,
Name: agent.ToolExecuteTest,
Response: resultMap,
},
}
m.conversationHistory = append(m.conversationHistory, chatMsg)

// Save function response to conversation
m.saveChatMessageToConversation(chatMsg)

// Send back to agent to continue
return m.sendChatMessage("")
}
return nil // No tool_use, so don't send response back
}
}

if toolName == agent.ToolExecuteTestGroup {
// Display results from test group
Expand Down
19 changes: 8 additions & 11 deletions internal/cli/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -1442,13 +1442,6 @@ func handleCommandsState(m *TestUIModel, msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}
}

// showToolWidget displays a tool execution widget with title and details.
// Deprecated: use m.showToolMessage() instead.
func showToolWidget(m *TestUIModel, title string, details string) {
m.showToolMessage(title, details)
m.updateViewport()
}

// handleProcessToolCalls executes tool calls received from the agent.
func handleProcessToolCalls(m *TestUIModel, _ processToolCallsMsg) (tea.Model, tea.Cmd) {
if len(m.streamedToolCalls) > 0 {
Expand All @@ -1471,7 +1464,8 @@ func handleProcessToolCalls(m *TestUIModel, _ processToolCallsMsg) (tea.Model, t
}
}
details := strings.Join(endpointsList, ", ")
showToolWidget(m, "Getting endpoint details", details)
m.showToolMessage("Getting endpoint details", details)
m.updateViewport()
}

return m, m.executeTool(toolCall)
Expand Down Expand Up @@ -1532,7 +1526,8 @@ func handleProcessToolCalls(m *TestUIModel, _ processToolCallsMsg) (tea.Model, t
formatCount := len(exportsArg)
label := fmt.Sprintf("%d format(s)", formatCount)

showToolWidget(m, "Exporting tests", label)
m.showToolMessage("Exporting tests", label)
m.updateViewport()
m.agentState = StateUsingTool
m.animationFrame = 0
m.spinner.Style = lipgloss.NewStyle().Foreground(Theme.Primary)
Expand All @@ -1542,7 +1537,8 @@ func handleProcessToolCalls(m *TestUIModel, _ processToolCallsMsg) (tea.Model, t
m.currentTestToolID = toolCall.ID
m.currentTestToolName = agent.ToolGenerateReport

showToolWidget(m, "Generating PDF report", "")
m.showToolMessage("Generating PDF report", "")
m.updateViewport()
m.agentState = StateUsingTool
m.animationFrame = 0
m.spinner.Style = lipgloss.NewStyle().Foreground(Theme.Primary)
Expand All @@ -1563,7 +1559,8 @@ func handleProcessToolCalls(m *TestUIModel, _ processToolCallsMsg) (tea.Model, t
label = fmt.Sprintf("%ds delay — %s", seconds, reason)
}

showToolWidget(m, "Waiting", label)
m.showToolMessage("Waiting", label)
m.updateViewport()
m.agentState = StateUsingTool
m.animationFrame = 0
m.spinner.Style = lipgloss.NewStyle().Foreground(Theme.Warning)
Expand Down
7 changes: 5 additions & 2 deletions internal/cli/update_tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,11 @@ func handleRunNextTest(m *TestUIModel, _ runNextTestMsg) (tea.Model, tea.Cmd) {
}

if err != nil {
m.addMessage(fmt.Sprintf(" ✗ %s %s%s", methodFormatted, endpoint, authIndicator))
m.addMessage(m.subtleStyle.Render(fmt.Sprintf(" Error: %s", err.Error())))
m.addMessage(fmt.Sprintf(" %s %s %s%s", m.errorStyle.Render("✗"), methodFormatted, endpoint, authIndicator))
m.addMessage(m.subtleStyle.Render(fmt.Sprintf(" Error: %s", friendlyError(err))))
if m.isHeadless {
m.headlessExitCode = 1
}

m.testGroupResults = append(m.testGroupResults, map[string]any{
"method": method,
Expand Down
23 changes: 21 additions & 2 deletions internal/cli/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,27 @@ func (m *TestUIModel) recreateHeader() tea.Cmd {
return nil
}

// friendlyError returns a short, human-readable version of common HTTP errors.
func friendlyError(err error) string {
msg := err.Error()
if strings.Contains(msg, "connection refused") {
return "connection refused"
}
if strings.Contains(msg, "no such host") {
return "host not found"
}
if strings.Contains(msg, "i/o timeout") || strings.Contains(msg, "context deadline exceeded") {
return "request timed out"
}
if strings.Contains(msg, "certificate") || strings.Contains(msg, "x509") {
return "TLS/certificate error"
}
if strings.Contains(msg, "EOF") {
return "connection closed unexpectedly"
}
return msg
}

func (m *TestUIModel) shouldAskForConfirmation(toolName string) bool {
// Tools that are safe and don't need confirmation
// ExecuteTestGroup is safe - user already approved the plan via checkboxes
Expand Down Expand Up @@ -336,8 +357,6 @@ func (m *TestUIModel) loadConversationHistory() error {
displayName = "Executing tests"
case agent.ToolGenerateReport:
displayName = "Generating PDF report"
case agent.ToolExecuteTest:
displayName = "Executing test"
default:
displayName = fmt.Sprintf("Tool: %s", toolName)
}
Expand Down