1package modes
2
3import (
4 "fmt"
5 "strings"
6 "testing"
7
8 "github.com/bnema/zut/packages/tui"
9)
10
11func newNotesTestInteractive() *Interactive {
12 i := &Interactive{dirty: make(chan struct{}, 1)}
13 i.cfg.Theme = tui.Theme{
14 Muted: tui.Color256(8), Warning: tui.Color256(3), Error: tui.Color256(1),
15 Tool: tui.Color256(2), Accent: tui.Color256(4),
16 }
17 return i
18}
19
20func TestClearNotesRemovesOnlyOwnerNotes(t *testing.T) {
21 i := newNotesTestInteractive()
22
23 i.Notify("kagi", "info", "pending")
24 i.Notify("kagi", "success", "approved")
25 i.Notify("other", "info", "keep me")
26
27 if len(i.extNotes) != 3 {
28 t.Fatalf("expected 3 notes, got %d", len(i.extNotes))
29 }
30
31 i.ClearNotes("kagi")
32
33 if len(i.extNotes) != 1 {
34 t.Fatalf("expected 1 note after clear, got %d: %v", len(i.extNotes), i.extNotes)
35 }
36 if !strings.Contains(i.extNotes[0], "[other] ") {
37 t.Fatalf("expected the surviving note to belong to other, got %q", i.extNotes[0])
38 }
39}
40
41func TestClearNotesNoMatchKeepsNotes(t *testing.T) {
42 i := newNotesTestInteractive()
43 i.Notify("kagi", "info", "pending")
44
45 i.ClearNotes("nope")
46
47 if len(i.extNotes) != 1 {
48 t.Fatalf("expected note to survive, got %d", len(i.extNotes))
49 }
50}
51
52func TestPersistentExtensionWidgetRowsAreBounded(t *testing.T) {
53 i := newNotesTestInteractive()
54 lines := make([]string, 20)
55 for idx := range lines {
56 lines[idx] = "line"
57 }
58 i.SetWidget("extension", "widget", "above_input", "Title", lines)
59
60 i.mu.Lock()
61 got := i.extensionChromeLinesLocked(80)
62 i.mu.Unlock()
63 if len(got) != maxExtensionWidgetRows {
64 t.Fatalf("widget rows = %d, want %d: %v", len(got), maxExtensionWidgetRows, got)
65 }
66 if !strings.Contains(strings.Join(got, "\n"), "extension widgets truncated") {
67 t.Fatalf("bounded widget rows omitted truncation marker: %v", got)
68 }
69}
70
71func TestPersistentExtensionStatusRowsAreBounded(t *testing.T) {
72 i := newNotesTestInteractive()
73 for n := 0; n < maxExtensionStatusRows+4; n++ {
74 i.SetStatus("extension", fmt.Sprintf("status-%d", n), "info", "line")
75 }
76
77 i.mu.Lock()
78 got := i.extensionChromeLinesLocked(80)
79 i.mu.Unlock()
80 if len(got) != maxExtensionStatusRows {
81 t.Fatalf("status rows = %d, want %d: %v", len(got), maxExtensionStatusRows, got)
82 }
83 if !strings.Contains(strings.Join(got, "\n"), "extension statuses truncated") {
84 t.Fatalf("bounded status rows omitted truncation marker: %v", got)
85 }
86}
87
88func TestPersistentExtensionStatusAndWidget(t *testing.T) {
89 i := newNotesTestInteractive()
90 i.SetStatus("sample", "progress", "success", "2/4 tasks checked")
91 i.SetWidget("sample", "plan", "above_input", "Plan", []string{"Current phase: parse", "[ ] read files"})
92
93 i.mu.Lock()
94 lines := i.extensionChromeLinesLocked(80)
95 statusKey := i.extensionStatusesKeyLocked()
96 widgetKey := i.extensionWidgetsKeyLocked()
97 i.mu.Unlock()
98 joined := strings.Join(lines, "\n")
99 if !strings.Contains(joined, "2/4 tasks checked") || !strings.Contains(joined, "Plan") || !strings.Contains(joined, "[ ] read files") {
100 t.Fatalf("persistent extension chrome = %q", joined)
101 }
102 if !strings.Contains(statusKey, "sample/progress/success/2/4 tasks checked") {
103 t.Fatalf("status cache key = %q", statusKey)
104 }
105 if !strings.Contains(widgetKey, "sample/plan/above_input/Plan") {
106 t.Fatalf("widget cache key = %q", widgetKey)
107 }
108
109 i.SetStatus("sample", "progress", "", "")
110 i.ClearWidget("sample", "plan")
111 if len(i.extStatuses) != 0 || len(i.extWidgets) != 0 {
112 t.Fatalf("persistent extension chrome was not cleared: statuses=%v widgets=%v", i.extStatuses, i.extWidgets)
113 }
114}
115
116func TestClearExtensionChromeRemovesOnlyTheExitedExtension(t *testing.T) {
117 i := newNotesTestInteractive()
118 i.SetStatus("gone", "progress", "success", "done")
119 i.SetWidget("gone", "plan", "above_input", "Gone", []string{"old"})
120 i.SetStatus("keep", "progress", "info", "still here")
121 i.SetWidget("keep", "plan", "above_input", "Keep", []string{"new"})
122 i.extNotes = []string{"[gone] old note", "[keep] keep note"}
123
124 i.ClearExtensionChrome("gone")
125
126 if _, ok := i.extStatuses["gone"]; ok {
127 t.Fatal("exited extension status survived")
128 }
129 if _, ok := i.extWidgets["gone"]; ok {
130 t.Fatal("exited extension widget survived")
131 }
132 if _, ok := i.extStatuses["keep"]; !ok {
133 t.Fatal("unrelated extension status was removed")
134 }
135 if _, ok := i.extWidgets["keep"]; !ok {
136 t.Fatal("unrelated extension widget was removed")
137 }
138 if got := strings.Join(i.extNotes, "\n"); got != "[keep] keep note" {
139 t.Fatalf("extension notes after cleanup = %q", got)
140 }
141}
142
143func TestInteractiveRedrawPlacesWidgetAboveInput(t *testing.T) {
144 term := &alertTestTerminal{}
145 i := NewInteractive(InteractiveConfig{Terminal: term, Theme: tui.Dark})
146 i.rend.Resize(80, 24)
147 i.SetWidget("sample", "plan", "above_input", "Plan", []string{"[ ] read files"})
148 i.redraw()
149
150 out := stripANSIBytes(term.String())
151 if !strings.Contains(out, "Plan") || !strings.Contains(out, "[ ] read files") {
152 t.Fatalf("redraw omitted above-input widget: %q", out)
153 }
154}