package ui
import (
"bytes"
"encoding/json"
"strings"
"testing"
"codey/internal/agent"
"codey/internal/llm"
)
func TestSummarizeInputIsSortedAndBounded(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{name: "sorted keys", in: `{"path":"main.go","offset":10}`, want: "offset=10, path=main.go"},
{name: "empty input", in: `{}`, want: ""},
{name: "unparseable falls back to raw", in: `not json`, want: "not json"},
{name: "long value truncated", in: `{"cmd":"` + strings.Repeat("x", 80) + `"}`, want: "cmd=" + strings.Repeat("x", 60) + "…"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := summarizeInput(json.RawMessage(tc.in)); got != tc.want {
t.Errorf("got %q, want %q", got, tc.want)
}
})
}
}
func TestToolResultIsSummarizedUnlessVerbose(t *testing.T) {
long := strings.Join([]string{"1", "2", "3", "4", "5", "6"}, "\n")
ev := agent.Event{Kind: agent.EventToolResult, Tool: "read_file", Result: long}
var quiet, loud bytes.Buffer
New(&quiet).Handle(ev)
(&Renderer{W: &loud, Verbose: true}).Handle(ev)
if !strings.Contains(quiet.String(), "2 more lines") {
t.Errorf("default should summarize, got:\n%s", quiet.String())
}
if strings.Contains(loud.String(), "more lines") || !strings.Contains(loud.String(), "6") {
t.Errorf("verbose should print everything, got:\n%s", loud.String())
}
}
func TestErrorResultIsLabelled(t *testing.T) {
var buf bytes.Buffer
New(&buf).Handle(agent.Event{Kind: agent.EventToolResult, Result: "no such file", IsError: true})
if !strings.Contains(buf.String(), "error: no such file") {
t.Errorf("got %q, want the failure marked", buf.String())
}
}
func TestUsageLine(t *testing.T) {
var buf bytes.Buffer
New(&buf).Usage(llm.Usage{InputTokens: 1200, OutputTokens: 300, CacheReadInputTokens: 900}, 4, "claude-opus-5")
got := buf.String()
for _, want := range []string{"4 turns", "900 cached", "$"} {
if !strings.Contains(got, want) {
t.Errorf("usage line %q is missing %q", got, want)
}
}
}
func TestUsageOmitsCostForAnUnknownModel(t *testing.T) {
var buf bytes.Buffer
New(&buf).Usage(llm.Usage{InputTokens: 10}, 1, "some-gateway-model")
if strings.Contains(buf.String(), "$") {
t.Errorf("got %q: a made-up cost is worse than none", buf.String())
}
}
func TestStreamedTextIsClosedBeforeOtherOutput(t *testing.T) {
var buf bytes.Buffer
r := New(&buf)
r.Handle(agent.Event{Kind: agent.EventTextDelta, Text: "Let me "})
r.Handle(agent.Event{Kind: agent.EventTextDelta, Text: "look."})
r.Handle(agent.Event{Kind: agent.EventToolStart, Tool: "read_file", Input: []byte(`{"path":"a.go"}`)})
if !strings.HasPrefix(buf.String(), "Let me look.\n") {
t.Errorf("streamed text should be flushed onto its own line, got %q", buf.String())
}
}
func TestDiffResultIsRenderedAsADiff(t *testing.T) {
var buf bytes.Buffer
diff := "--- a.go\n+++ a.go\n@@ -1,2 +1,2 @@\n-old\n+new"
New(&buf).Handle(agent.Event{Kind: agent.EventToolResult, Tool: "edit_file", Result: diff})
got := buf.String()
if strings.Contains(got, "⎿") {
t.Errorf("a diff should not be summarized like ordinary tool output:\n%s", got)
}
for _, want := range []string{"-old", "+new", "@@ -1,2 +1,2 @@"} {
if !strings.Contains(got, want) {
t.Errorf("diff missing %q:\n%s", want, got)
}
}
}
func TestLooksLikeDiff(t *testing.T) {
if !looksLikeDiff("--- a.go\n+++ a.go\n@@ -1 +1 @@\n-x\n+y") {
t.Error("a unified diff should be recognised")
}
if looksLikeDiff("Created a.go (12 bytes, 1 lines).") {
t.Error("ordinary tool output should not be treated as a diff")
}
}