Skip to content

Commit 5f91c6e

Browse files
author
Zero
committed
feat(tui): clarify commands and add input modes
1 parent c5e1219 commit 5f91c6e

6 files changed

Lines changed: 633 additions & 64 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package tui
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/KKingZero/Cobra-AI/zypheron-go/internal/tui/components"
8+
"github.com/KKingZero/Cobra-AI/zypheron-go/internal/tui/views"
9+
tea "github.com/charmbracelet/bubbletea"
10+
"github.com/charmbracelet/lipgloss"
11+
)
12+
13+
func newDashboardTestModel() Model {
14+
model := NewModel()
15+
model.state = stateDashboard
16+
model.width = 100
17+
model.height = 36
18+
model.header = components.NewHeader()
19+
model.console = views.NewConsole(100, 20)
20+
model.summary = views.NewSummary(100)
21+
model.input = views.NewInput(100)
22+
model.modelSelector = views.NewModelSelector(100)
23+
model.applyInteractionMode()
24+
return model
25+
}
26+
27+
func TestTabKeysRenderDistinctContextPanels(t *testing.T) {
28+
testCases := []struct {
29+
key rune
30+
want string
31+
}{
32+
{'R', "Recon context is setup/status-only"},
33+
{'S', "Quick actions: /scan <target>"},
34+
{'A', "Active model:"},
35+
{'T', "Entry points: /tools, /doctor"},
36+
{'H', "No scan history is loaded"},
37+
}
38+
39+
seen := map[string]bool{}
40+
for _, tc := range testCases {
41+
model := newDashboardTestModel()
42+
updated, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{tc.key}})
43+
rendered := updated.(Model).View()
44+
if !strings.Contains(rendered, tc.want) {
45+
t.Fatalf("tab %q rendered content missing %q:\n%s", string(tc.key), tc.want, rendered)
46+
}
47+
if seen[rendered] {
48+
t.Fatalf("tab %q rendered duplicate context panel", string(tc.key))
49+
}
50+
seen[rendered] = true
51+
}
52+
}
53+
54+
func TestInitialEmptyDashboardKeepsWelcomeScreen(t *testing.T) {
55+
model := newDashboardTestModel()
56+
rendered := model.View()
57+
58+
if !strings.Contains(rendered, "Quick Start") {
59+
t.Fatalf("initial dashboard should render welcome quick start:\n%s", rendered)
60+
}
61+
if strings.Contains(rendered, "No active scan") {
62+
t.Fatalf("initial dashboard rendered scan context before tab navigation:\n%s", rendered)
63+
}
64+
}
65+
66+
func TestEmptyStatePanelFitsNarrowViewport(t *testing.T) {
67+
model := newDashboardTestModel()
68+
model.width = 30
69+
model.console = views.NewConsole(30, 10)
70+
71+
rendered := model.renderTabContextPanel()
72+
for _, line := range strings.Split(rendered, "\n") {
73+
if width := lipgloss.Width(line); width > model.width {
74+
t.Fatalf("line width = %d, want <= %d:\n%s", width, model.width, rendered)
75+
}
76+
}
77+
}
78+
79+
func TestModeInputFitsNarrowViewport(t *testing.T) {
80+
model := newDashboardTestModel()
81+
model.width = 30
82+
model.input = views.NewInput(30)
83+
model.applyInteractionMode()
84+
85+
rendered := model.renderModeInput()
86+
for _, line := range strings.Split(rendered, "\n") {
87+
if width := lipgloss.Width(line); width > model.width {
88+
t.Fatalf("line width = %d, want <= %d:\n%s", width, model.width, rendered)
89+
}
90+
}
91+
}
92+
93+
func TestShiftTabCyclesToPlanWithoutOpeningModelSelector(t *testing.T) {
94+
model := newDashboardTestModel()
95+
96+
updated, _ := model.Update(tea.KeyMsg{Type: tea.KeyShiftTab})
97+
got := updated.(Model)
98+
99+
if got.mode != modePlan {
100+
t.Fatalf("mode = %s, want Plan", got.mode.Label())
101+
}
102+
if got.modelSelector.IsOpen() {
103+
t.Fatal("shift+tab opened model selector; Tab should remain the model selector key")
104+
}
105+
if got.input.TextInput.Placeholder != modePlan.Placeholder() {
106+
t.Fatalf("placeholder = %q, want %q", got.input.TextInput.Placeholder, modePlan.Placeholder())
107+
}
108+
}
109+
110+
func TestHelpUsesSlashPrefixedCommandExamples(t *testing.T) {
111+
model := newDashboardTestModel()
112+
rendered := model.renderHelp()
113+
114+
for _, want := range []string{"/ai <question>", "/autopent <target>", "/dork <query>", "/scan <target>", "/recon <domain>"} {
115+
if !strings.Contains(rendered, want) {
116+
t.Fatalf("help missing %q in:\n%s", want, rendered)
117+
}
118+
}
119+
120+
for _, bare := range []string{" ai <question>", " autopent <target>", " dork <query>"} {
121+
if strings.Contains(rendered, bare) {
122+
t.Fatalf("help contains bare command example %q in:\n%s", bare, rendered)
123+
}
124+
}
125+
}
126+
127+
func TestReconCommandDocumentsStatusOnlyBehavior(t *testing.T) {
128+
model := newDashboardTestModel()
129+
130+
cmd := model.handleCommand("/recon example.com")
131+
if cmd != nil {
132+
t.Fatal("/recon returned a command; expected status/context-only behavior")
133+
}
134+
135+
rendered := model.console.View()
136+
if !strings.Contains(rendered, "Recon context set for example.com") {
137+
t.Fatalf("recon output missing context message:\n%s", rendered)
138+
}
139+
if !strings.Contains(rendered, "Next: /scan example.com --tool nmap") {
140+
t.Fatalf("recon output missing slash-prefixed next actions:\n%s", rendered)
141+
}
142+
}

0 commit comments

Comments
 (0)