package test import ( "fmt" "strings" "time" "github.com/fatih/color" jsoniter "github.com/json-iterator/go" ) // OutputWriter handles colored console output for test execution type OutputWriter struct { verbose bool } // NewOutputWriter creates a new output writer func NewOutputWriter(verbose bool) *OutputWriter { return &OutputWriter{verbose: verbose} } // Header prints a header section func (w *OutputWriter) Header(title string) { fmt.Println() color.New(color.FgCyan, color.Bold).Println("═══════════════════════════════════════════════════════════════") color.New(color.FgCyan, color.Bold).Printf(" %s\n", title) color.New(color.FgCyan, color.Bold).Println("═══════════════════════════════════════════════════════════════") } // SubHeader prints a sub-header func (w *OutputWriter) SubHeader(title string) { fmt.Println() color.New(color.FgWhite, color.Bold).Println("───────────────────────────────────────────────────────────────") color.New(color.FgWhite, color.Bold).Printf(" %s\n", title) color.New(color.FgWhite, color.Bold).Println("───────────────────────────────────────────────────────────────") } // Info prints an info message func (w *OutputWriter) Info(format string, args ...interface{}) { color.New(color.FgBlue).Printf("ℹ ") fmt.Printf(format+"\n", args...) } // Success prints a success message func (w *OutputWriter) Success(format string, args ...interface{}) { color.New(color.FgGreen).Printf("✓ ") fmt.Printf(format+"\n", args...) } // Error prints an error message func (w *OutputWriter) Error(format string, args ...interface{}) { color.New(color.FgRed).Printf("✗ ") fmt.Printf(format+"\n", args...) } // Warning prints a warning message func (w *OutputWriter) Warning(format string, args ...interface{}) { color.New(color.FgYellow).Printf("⚠ ") fmt.Printf(format+"\n", args...) } // Skip prints a skip message func (w *OutputWriter) Skip(format string, args ...interface{}) { color.New(color.FgYellow).Printf("○ ") fmt.Printf(format+"\n", args...) } // Verbose prints a verbose message (only if verbose mode is enabled) func (w *OutputWriter) Verbose(format string, args ...interface{}) { if w.verbose { color.New(color.FgHiBlack).Printf(" │ ") fmt.Printf(format+"\n", args...) } } // TestStart prints test case start func (w *OutputWriter) TestStart(id string, input string, runNum int) { inputPreview := truncateString(input, 50) if runNum > 1 { color.New(color.FgWhite).Printf("► [%s] Run %d: ", id, runNum) } else { color.New(color.FgWhite).Printf("► [%s] ", id) } color.New(color.FgHiBlack).Printf("%s", inputPreview) fmt.Print(" ") } // TestResult prints test case result func (w *OutputWriter) TestResult(status Status, duration time.Duration) { switch status { case StatusPassed: color.New(color.FgGreen, color.Bold).Printf("PASSED") case StatusFailed: color.New(color.FgRed, color.Bold).Printf("FAILED") case StatusSkipped: color.New(color.FgYellow).Printf("SKIPPED") case StatusError: color.New(color.FgRed, color.Bold).Printf("ERROR") case StatusTimeout: color.New(color.FgRed).Printf("TIMEOUT") } color.New(color.FgHiBlack).Printf(" (%s)\n", formatDuration(duration)) } // TestError prints test error details func (w *OutputWriter) TestError(err string) { color.New(color.FgRed).Printf(" └─ %s\n", err) } // TestOutput prints test output (verbose mode) func (w *OutputWriter) TestOutput(output string) { if w.verbose && output != "" { outputPreview := truncateString(output, 100) color.New(color.FgHiBlack).Printf(" └─ Output: %s\n", outputPreview) } } // Progress prints progress information func (w *OutputWriter) Progress(current, total int) { percentage := float64(current) / float64(total) * 100 color.New(color.FgHiBlack).Printf("\r Progress: %d/%d (%.0f%%)", current, total, percentage) } // Summary prints the test summary func (w *OutputWriter) Summary(summary *Summary, duration time.Duration) { w.SubHeader("Summary") // Agent info color.New(color.FgWhite).Printf(" Agent: ") color.New(color.FgCyan).Printf("%s\n", summary.AgentID) if summary.Connector != "" { color.New(color.FgWhite).Printf(" Connector: ") color.New(color.FgCyan).Printf("%s\n", summary.Connector) } // Results color.New(color.FgWhite).Printf(" Total: ") fmt.Printf("%d\n", summary.Total) color.New(color.FgWhite).Printf(" Passed: ") if summary.Passed > 0 { color.New(color.FgGreen).Printf("%d\n", summary.Passed) } else { fmt.Printf("%d\n", summary.Passed) } color.New(color.FgWhite).Printf(" Failed: ") if summary.Failed > 0 { color.New(color.FgRed).Printf("%d\n", summary.Failed) } else { fmt.Printf("%d\n", summary.Failed) } if summary.Skipped > 0 { color.New(color.FgWhite).Printf(" Skipped: ") color.New(color.FgYellow).Printf("%d\n", summary.Skipped) } if summary.Errors > 0 { color.New(color.FgWhite).Printf(" Errors: ") color.New(color.FgRed).Printf("%d\n", summary.Errors) } if summary.Timeouts > 0 { color.New(color.FgWhite).Printf(" Timeouts: ") color.New(color.FgRed).Printf("%d\n", summary.Timeouts) } // Pass rate passRate := float64(0) if summary.Total > 0 { passRate = float64(summary.Passed) / float64(summary.Total) * 100 } color.New(color.FgWhite).Printf(" Pass Rate: ") if passRate == 100 { color.New(color.FgGreen, color.Bold).Printf("%.1f%%\n", passRate) } else if passRate >= 80 { color.New(color.FgYellow).Printf("%.1f%%\n", passRate) } else { color.New(color.FgRed).Printf("%.1f%%\n", passRate) } // Duration color.New(color.FgWhite).Printf(" Duration: ") fmt.Printf("%s\n", formatDuration(duration)) // Stability info (if runs > 1) if summary.RunsPerCase > 1 { fmt.Println() color.New(color.FgWhite, color.Bold).Println(" Stability Analysis:") color.New(color.FgWhite).Printf(" Runs/Case: %d\n", summary.RunsPerCase) color.New(color.FgWhite).Printf(" Total Runs: %d\n", summary.TotalRuns) color.New(color.FgWhite).Printf(" Stable Cases: ") if summary.StableCases == summary.Total { color.New(color.FgGreen).Printf("%d\n", summary.StableCases) } else { color.New(color.FgYellow).Printf("%d\n", summary.StableCases) } color.New(color.FgWhite).Printf(" Unstable: ") if summary.UnstableCases > 0 { color.New(color.FgRed).Printf("%d\n", summary.UnstableCases) } else { fmt.Printf("%d\n", summary.UnstableCases) } } } // OutputFile prints the output file path func (w *OutputWriter) OutputFile(path string) { fmt.Println() color.New(color.FgWhite).Printf(" Output: ") color.New(color.FgCyan).Printf("%s\n", path) } // FinalResult prints the final result banner func (w *OutputWriter) FinalResult(passed bool) { fmt.Println() if passed { color.New(color.FgGreen, color.Bold).Println("═══════════════════════════════════════════════════════════════") color.New(color.FgGreen, color.Bold).Println(" ✨ ALL TESTS PASSED ✨") color.New(color.FgGreen, color.Bold).Println("═══════════════════════════════════════════════════════════════") } else { color.New(color.FgRed, color.Bold).Println("═══════════════════════════════════════════════════════════════") color.New(color.FgRed, color.Bold).Println(" ❌ TESTS FAILED") color.New(color.FgRed, color.Bold).Println("═══════════════════════════════════════════════════════════════") } fmt.Println() } // DirectOutput prints the agent output directly (for development mode) func (w *OutputWriter) DirectOutput(output interface{}) { if output == nil { return } // Try to format as JSON if it's a complex type switch v := output.(type) { case string: fmt.Println(v) case map[string]interface{}, []interface{}: // Pretty print JSON jsonBytes, err := jsoniter.MarshalIndent(v, "", " ") if err != nil { fmt.Printf("%v\n", output) } else { fmt.Println(string(jsonBytes)) } default: // Try to marshal as JSON jsonBytes, err := jsoniter.MarshalIndent(output, "", " ") if err != nil { fmt.Printf("%v\n", output) } else { fmt.Println(string(jsonBytes)) } } } // DirectOutputJSON outputs a complete JSON object with output, trace and duration. // Designed for AI/script consumption via --json flag. func (w *OutputWriter) DirectOutputJSON(output interface{}, trace *Trace, duration time.Duration) { payload := map[string]interface{}{ "output": output, "duration_ms": duration.Milliseconds(), } if trace != nil { payload["trace"] = trace } jsonBytes, err := jsoniter.MarshalIndent(payload, "", " ") if err != nil { fmt.Printf("%v\n", output) return } fmt.Println(string(jsonBytes)) } // DirectTrace prints a human-readable summary of tool calls from a Trace. // Only prints when there are tool calls; skipped when trace is nil or empty. func (w *OutputWriter) DirectTrace(trace *Trace) { if trace == nil || len(trace.ToolCalls) == 0 { return } fmt.Println() color.New(color.FgHiBlack).Println("--- Tool Calls ---") for _, tc := range trace.ToolCalls { prefix := tc.Tool if tc.Server != "" { prefix = tc.Server + "/" + tc.Tool } status := "OK" if tc.Error != "" { status = "ERR: " + truncateString(tc.Error, 60) } argsStr := "" if tc.Arguments != nil { if b, err := jsoniter.Marshal(tc.Arguments); err == nil { argsStr = truncateString(string(b), 80) } } if argsStr != "" { color.New(color.FgHiBlack).Printf(" %s → %s (args: %s)\n", prefix, status, argsStr) } else { color.New(color.FgHiBlack).Printf(" %s → %s\n", prefix, status) } } } // ScriptOutputJSON outputs the complete script test report as JSON. func (w *OutputWriter) ScriptOutputJSON(report *ScriptTestReport) { jsonBytes, err := jsoniter.MarshalIndent(report, "", " ") if err != nil { fmt.Printf("{\"error\": %q}\n", err.Error()) return } fmt.Println(string(jsonBytes)) } // ScriptTestSummary prints the script test summary func (w *OutputWriter) ScriptTestSummary(summary *ScriptTestSummary, duration time.Duration) { w.SubHeader("Summary") // Results color.New(color.FgWhite).Printf(" Total: ") fmt.Printf("%d\n", summary.Total) color.New(color.FgWhite).Printf(" Passed: ") if summary.Passed > 0 { color.New(color.FgGreen).Printf("%d\n", summary.Passed) } else { fmt.Printf("%d\n", summary.Passed) } color.New(color.FgWhite).Printf(" Failed: ") if summary.Failed > 0 { color.New(color.FgRed).Printf("%d\n", summary.Failed) } else { fmt.Printf("%d\n", summary.Failed) } if summary.Skipped > 0 { color.New(color.FgWhite).Printf(" Skipped: ") color.New(color.FgYellow).Printf("%d\n", summary.Skipped) } // Pass rate passRate := float64(0) if summary.Total > 0 { passRate = float64(summary.Passed) / float64(summary.Total) * 100 } color.New(color.FgWhite).Printf(" Pass Rate: ") if passRate == 100 { color.New(color.FgGreen, color.Bold).Printf("%.1f%%\n", passRate) } else if passRate >= 80 { color.New(color.FgYellow).Printf("%.1f%%\n", passRate) } else { color.New(color.FgRed).Printf("%.1f%%\n", passRate) } // Duration color.New(color.FgWhite).Printf(" Duration: ") fmt.Printf("%s\n", formatDuration(duration)) } // DynamicTestStart outputs the start of a dynamic test func (w *OutputWriter) DynamicTestStart(id string, checkpointCount int) { color.New(color.FgWhite).Printf("► [%s] ", id) color.New(color.FgCyan).Printf("(dynamic, %d checkpoints)\n", checkpointCount) } // DynamicTurn outputs a single turn in dynamic testing func (w *OutputWriter) DynamicTurn(turn int, inputSummary string, checkpointsReached, total int) { if w.verbose { color.New(color.FgHiBlack).Printf("│ ├─ Turn %d: %s ", turn, inputSummary) color.New(color.FgCyan).Printf("[%d/%d checkpoints]\n", checkpointsReached, total) } } // DynamicCheckpoint outputs a checkpoint being reached func (w *OutputWriter) DynamicCheckpoint(checkpointID string) { if w.verbose { color.New(color.FgGreen).Printf("│ │ └─ ✓ checkpoint: %s\n", checkpointID) } } // DynamicTestResult outputs the result of a dynamic test func (w *OutputWriter) DynamicTestResult(status Status, turns int, checkpoints int, duration time.Duration) { color.New(color.FgHiBlack).Printf(" └─ ") switch status { case StatusPassed: color.New(color.FgGreen).Printf("PASSED") case StatusFailed: color.New(color.FgRed).Printf("FAILED") case StatusError: color.New(color.FgRed).Printf("ERROR") case StatusTimeout: color.New(color.FgRed).Printf("TIMEOUT") } color.New(color.FgHiBlack).Printf(" (%d turns, %d checkpoints, %s)\n", turns, checkpoints, formatDuration(duration)) } // StabilityResult prints stability analysis result for a test case func (w *OutputWriter) StabilityResult(sr *StabilityResult) { color.New(color.FgWhite).Printf(" [%s] ", sr.ID) // Pass rate if sr.PassRate == 100 { color.New(color.FgGreen).Printf("%.0f%%", sr.PassRate) } else if sr.PassRate >= 80 { color.New(color.FgYellow).Printf("%.0f%%", sr.PassRate) } else { color.New(color.FgRed).Printf("%.0f%%", sr.PassRate) } // Classification color.New(color.FgHiBlack).Printf(" (%d/%d) ", sr.Passed, sr.Runs) switch sr.StabilityClass { case StabilityStable: color.New(color.FgGreen).Printf("Stable") case StabilityMostlyStable: color.New(color.FgYellow).Printf("Mostly Stable") case StabilityUnstable: color.New(color.FgRed).Printf("Unstable") case StabilityHighlyUnstable: color.New(color.FgRed, color.Bold).Printf("Highly Unstable") } // Timing color.New(color.FgHiBlack).Printf(" avg:%.0fms\n", sr.AvgDurationMs) } // Helper functions func truncateString(s string, maxLen int) string { // Remove newlines and extra spaces s = strings.ReplaceAll(s, "\n", " ") s = strings.ReplaceAll(s, "\r", "") s = strings.Join(strings.Fields(s), " ") if len(s) <= maxLen { return s } return s[:maxLen-3] + "..." } func formatDuration(d time.Duration) string { if d < time.Millisecond { return fmt.Sprintf("%dµs", d.Microseconds()) } if d < time.Second { return fmt.Sprintf("%dms", d.Milliseconds()) } if d < time.Minute { return fmt.Sprintf("%.1fs", d.Seconds()) } return fmt.Sprintf("%.1fm", d.Minutes()) }