1package tui
2
3import (
4 "fmt"
5 "strings"
6 "testing"
7
8 "github.com/bnema/zut/packages/provider"
9)
10
11func TestViewCacheInvalidatesToolOutputExpansionAndWidth(t *testing.T) {
12 messages := []provider.Message{
13 {
14 Role: provider.RoleAssistant,
15 Content: []provider.Content{provider.ToolCallBlock{
16 ID: "call-1", Name: "bash", Arguments: []byte(`{"command":"cat file"}`),
17 }},
18 },
19 {
20 Role: provider.RoleTool,
21 Content: []provider.Content{provider.ToolResultBlock{
22 CallID: "call-1",
23 Content: []provider.Content{provider.TextBlock{Text: cacheTestResult("old")}},
24 }},
25 },
26 }
27 view := &View{Theme: Dark, Messages: messages, MessagesRevision: 1}
28 collapsed, _ := view.BuildWithAnchors(40)
29 if len(collapsed) == 0 || !strings.Contains(strings.Join(collapsed, "\n"), "more lines") {
30 t.Fatal("long tool result did not use the collapsed presentation")
31 }
32
33 view.ExpandAll = true
34 expanded, _ := view.BuildWithAnchors(40)
35 if len(expanded) <= len(collapsed) {
36 t.Fatalf("expansion did not invalidate rows: collapsed=%d expanded=%d", len(collapsed), len(expanded))
37 }
38
39 wide, _ := view.BuildWithAnchors(120)
40 // The wide render may have the same row count for this fixture, but it
41 // must still be rebuilt for the new wrapping width.
42 if strings.Join(wide, "\n") == strings.Join(expanded, "\n") {
43 t.Fatal("resize reused rows from the old width")
44 }
45
46 view.ExpandAll = false
47 view.Messages[1].Content = []provider.Content{provider.ToolResultBlock{
48 CallID: "call-1",
49 Content: []provider.Content{provider.TextBlock{Text: cacheTestResult("new marker")}},
50 }}
51 view.Messages = append(view.Messages, provider.Message{
52 Role: provider.RoleUser,
53 Content: []provider.Content{provider.TextBlock{Text: "third message"}},
54 })
55 view.MessagesRevision = 2
56 updated, _ := view.BuildWithAnchors(40)
57 if !strings.Contains(strings.Join(updated, "\n"), "new marker") {
58 t.Fatal("tool output revision reused stale cached rows")
59 }
60}
61
62func TestViewRevisionCacheIsBoundToTailLimit(t *testing.T) {
63 messages := make([]provider.Message, 20)
64 for i := range messages {
65 messages[i] = provider.Message{
66 Role: provider.RoleUser,
67 Content: []provider.Content{provider.TextBlock{Text: fmt.Sprintf("message %d", i)}},
68 }
69 }
70 view := &View{Theme: Dark, Messages: messages, MessagesRevision: 1, TailLimit: 5}
71 view.Build(80)
72 if view.messageCacheStart != len(messages)-view.TailLimit {
73 t.Fatalf("message cache start=%d, want %d", view.messageCacheStart, len(messages)-view.TailLimit)
74 }
75 if len(view.messageCache) > view.TailLimit {
76 t.Fatalf("message cache length=%d exceeds tail limit %d", len(view.messageCache), view.TailLimit)
77 }
78
79 view.Build(80)
80 if len(view.messageCache) > view.TailLimit {
81 t.Fatalf("message cache grew on a cache hit: length=%d", len(view.messageCache))
82 }
83}
84
85func TestRenderLiveToolResultCollapseUsesRenderedLineCount(t *testing.T) {
86 var text strings.Builder
87 for i := 0; i < 13; i++ {
88 text.WriteString(strings.Repeat("x", 17))
89 text.WriteByte('\n')
90 }
91 view := &View{Theme: Dark}
92 full := view.renderToolText(text.String(), 20, Dark.ToolOut, "", 1)
93 collapsed := view.renderLiveToolResult(text.String(), 20, Dark.ToolOut, "")
94 joined := strings.Join(collapsed, "\n")
95 wantTotal := fmt.Sprintf("%d total", len(full))
96 if !strings.Contains(joined, wantTotal) {
97 t.Fatalf("collapse footer omitted rendered total %q: %s", wantTotal, joined)
98 }
99}
100
101func cacheTestResult(prefix string) string {
102 var b strings.Builder
103 for n := 0; n < 32; n++ {
104 b.WriteString(prefix)
105 b.WriteString(" line ")
106 b.WriteString(string(rune('a' + n%26)))
107 b.WriteByte('\n')
108 }
109 return b.String()
110}