From e561efe7929e00e79467248823479c815c225037 Mon Sep 17 00:00:00 2001 From: Oscar <319719771+Oscar-Williams@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:31:16 +0800 Subject: [PATCH 1/2] fix(tui/live): remove advertised session-picker key with no handler The Live help table listed "s" as a session picker, but onKey never handled that key. Drop the misleading binding and add a regression test that every advertised help key has a handler. Closes #148 Signed-off-by: Oscar <319719771+Oscar-Williams@users.noreply.github.com> --- packages/tui/live/live_keys_test.go | 42 ++++++++++++++++++++++++++++- packages/tui/live/live_view.go | 3 +-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/tui/live/live_keys_test.go b/packages/tui/live/live_keys_test.go index b7bd4fe..da7db0a 100644 --- a/packages/tui/live/live_keys_test.go +++ b/packages/tui/live/live_keys_test.go @@ -519,4 +519,44 @@ 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) } -} \ No newline at end of file +} + +func TestLiveHelpAdvertisedKeysHaveHandlers(t *testing.T) { + // Single-token Stream help keys must be backed by Live.onKey, or by the + // app shell for esc/q. Composite navigation labels ("↑↓ / PgUp PgDn") and + // the meta "any key" entry are covered by their own tests. + handled := map[string]struct{}{ + "w": {}, "b": {}, "d": {}, "x": {}, "r": {}, + "/": {}, "l": {}, "e": {}, + "enter": {}, "space": {}, + "esc": {}, "q": {}, // App.onKey (packages/tui/app_keys.go) + "?": {}, + "←": {}, + } + skipLabel := map[string]struct{}{ + "↑↓ / PgUp PgDn": {}, + "any key": {}, + } + + for _, g := range liveHelp { + for _, pair := range g.Keys { + label, desc := pair[0], pair[1] + if _, ok := skipLabel[label]; ok { + continue + } + if _, ok := handled[label]; ok { + continue + } + for _, tok := range strings.Split(label, "/") { + tok = strings.TrimSpace(tok) + if tok == "" { + continue + } + if _, ok := handled[tok]; ok { + continue + } + t.Errorf("liveHelp advertises %q (%q) with no handler", tok, desc) + } + } + } +} diff --git a/packages/tui/live/live_view.go b/packages/tui/live/live_view.go index 4248b0a..9c198cc 100644 --- a/packages/tui/live/live_view.go +++ b/packages/tui/live/live_view.go @@ -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"}, @@ -831,4 +830,4 @@ func maxFloat(a, b float64) float64 { return a } return b -} \ No newline at end of file +} From fffaa406dad548366b4493d8da32c2e8d3c642bb Mon Sep 17 00:00:00 2001 From: Oscar <319719771+Oscar-Williams@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:40:09 +0800 Subject: [PATCH 2/2] test(tui/live): drive help-key coverage through onKey Rewrite TestLiveHelpAdvertisedKeysHaveHandlers to construct a KeyMsg for each advertised help token, call onKey, and require a Live state change relative to an unknown-key baseline. Drop the parallel handled map so a fake help row without a handler fails the test. Signed-off-by: Oscar <319719771+Oscar-Williams@users.noreply.github.com> --- packages/tui/live/live_keys_test.go | 127 +++++++++++++++++++++++----- 1 file changed, 105 insertions(+), 22 deletions(-) diff --git a/packages/tui/live/live_keys_test.go b/packages/tui/live/live_keys_test.go index da7db0a..248ae01 100644 --- a/packages/tui/live/live_keys_test.go +++ b/packages/tui/live/live_keys_test.go @@ -7,6 +7,7 @@ import ( "encoding/json" "os" "path/filepath" + "strconv" "strings" "testing" "time" @@ -521,22 +522,79 @@ func TestExportMkdirFailureSetsErrorFlash(t *testing.T) { } } -func TestLiveHelpAdvertisedKeysHaveHandlers(t *testing.T) { - // Single-token Stream help keys must be backed by Live.onKey, or by the - // app shell for esc/q. Composite navigation labels ("↑↓ / PgUp PgDn") and - // the meta "any key" entry are covered by their own tests. - handled := map[string]struct{}{ - "w": {}, "b": {}, "d": {}, "x": {}, "r": {}, - "/": {}, "l": {}, "e": {}, - "enter": {}, "space": {}, - "esc": {}, "q": {}, // App.onKey (packages/tui/app_keys.go) - "?": {}, - "←": {}, +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 { @@ -544,18 +602,43 @@ func TestLiveHelpAdvertisedKeysHaveHandlers(t *testing.T) { if _, ok := skipLabel[label]; ok { continue } - if _, ok := handled[label]; ok { - continue - } - for _, tok := range strings.Split(label, "/") { - tok = strings.TrimSpace(tok) - if tok == "" { - 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) + } } - if _, ok := handled[tok]; ok { - continue - } - t.Errorf("liveHelp advertises %q (%q) with no handler", tok, desc) + } + 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) + } + }) } } }