Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 124 additions & 1 deletion packages/tui/live/live_keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -519,4 +520,126 @@ func TestExportMkdirFailureSetsErrorFlash(t *testing.T) {
if !strings.HasPrefix(p.action.text, "✗ export failed:") {
t.Errorf("action.text = %q, want it to start with the export-failed prefix", p.action.text)
}
}
}

func liveHelpKeyMsg(token string) (tea.KeyMsg, bool) {
switch token {
case "space":
return tea.KeyMsg{Type: tea.KeySpace}, true
case "enter":
return tea.KeyMsg{Type: tea.KeyEnter}, true
case "←":
return tea.KeyMsg{Type: tea.KeyLeft}, true
default:
if len([]rune(token)) != 1 {
return tea.KeyMsg{}, false
}
return runeKey(token), true
}
}

func liveSnap(p *Live) string {
action := ""
if p.action != nil {
action = p.action.level + "|" + p.action.text
}
inspect := ""
if p.inspect != nil {
inspect = p.inspect.ID
}
return strings.Join([]string{
"mode=" + p.mode,
"signal=" + p.signal,
"sel=" + strconv.Itoa(p.sel),
"help=" + strconv.FormatBool(p.help),
"frozen=" + strconv.FormatBool(p.frozen),
"searchEdit=" + strconv.FormatBool(p.editingSearch),
"inspect=" + inspect,
"layersScroll=" + strconv.Itoa(p.layersScroll),
"action=" + action,
}, ";")
}

func preparedLiveForHelpKey(t *testing.T, token string) *Live {
t.Helper()
p := newTestLiveForKeys()
switch token {
case "w":
p.merged = []streamItem{{ID: "d1", Decision: "DENY", Tool: "Bash"}}
case "b":
p.merged = []streamItem{{ID: "a1", Decision: "ALLOW", Tool: "Read"}}
case "enter", "e":
p.merged = []streamItem{{ID: "row", Decision: "ALLOW", Tool: "Read"}}
if token == "e" {
t.Setenv("HOME", t.TempDir())
}
case "←":
p.mode = "inspect"
p.inspect = &streamItem{ID: "row", Decision: "ALLOW", Tool: "Read"}
p.inspectScroll = 3
default:
p.merged = []streamItem{{ID: "row", Decision: "ALLOW", Tool: "Read"}}
}
return p
}

func TestLiveHelpAdvertisedKeysHaveHandlers(t *testing.T) {
// Every discrete help token must change Live state through onKey.
// Composite navigation labels and the meta "any key" line are covered
// elsewhere. esc/q are handled by App.onKey, not Live.onKey.
skipLabel := map[string]struct{}{
"↑↓ / PgUp PgDn": {},
"any key": {},
"esc": {},
"q": {},
}
ctx := tui.PanelContext{Now: time.UnixMilli(1000), Rows: 20, Cols: 80}
unknown := runeKey("=")

for _, g := range liveHelp {
for _, pair := range g.Keys {
label, desc := pair[0], pair[1]
if _, ok := skipLabel[label]; ok {
continue
}
tokens := []string{label}
if strings.Contains(label, " / ") {
tokens = nil
for _, part := range strings.Split(label, "/") {
part = strings.TrimSpace(part)
if part != "" {
tokens = append(tokens, part)
}
}
}
for _, tok := range tokens {
tok, desc := tok, desc
t.Run(tok, func(t *testing.T) {
msg, ok := liveHelpKeyMsg(tok)
if !ok {
t.Fatalf("help token %q (%q) has no KeyMsg mapping", tok, desc)
}

pKey := preparedLiveForHelpKey(t, tok)
before := liveSnap(pKey)
pKey.onKey(msg, ctx)
afterKey := liveSnap(pKey)

pUnk := preparedLiveForHelpKey(t, tok)
pUnk.onKey(unknown, ctx)
afterUnk := liveSnap(pUnk)

if afterUnk != before {
t.Fatalf("baseline unknown key %q changed state: before=%s after=%s", "=", before, afterUnk)
}
if afterKey == before {
t.Errorf("liveHelp advertises %q (%q) but onKey left Live state unchanged (same as unknown key)", tok, desc)
}
if afterKey == afterUnk {
t.Errorf("liveHelp advertises %q (%q) but onKey matched the unknown-key baseline", tok, desc)
}
})
}
}
}
}
3 changes: 1 addition & 2 deletions packages/tui/live/live_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ var liveHelp = []tui.HelpGroup{
{"b", "block the selected ALLOW (adds DENY rule)"},
{"d / x / r", "filter: denies / dlp hits / rate-limit bursts"},
{"/", "live search (tool, agent, command…) · enter done"},
{"s", "session picker"},
{"l", "layers detail (rate limit · dlp · guard)"},
{"e", "export visible rows → ~/.solongate/live-export.jsonl"},
{"space", "copy mode: freeze screen for mouse selection"},
Expand Down Expand Up @@ -831,4 +830,4 @@ func maxFloat(a, b float64) float64 {
return a
}
return b
}
}
Loading