Vectrum
Sign In
110 lines · 3.5 KB · Go
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")
	}
}