package ui import ( "bytes" "encoding/json" "strings" "testing" "github.com/johannSo/codey/internal/agent" "github.com/johannSo/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") } }