Compare commits

..
Author SHA1 Message Date
Gnanam 8b7b8296ac tui: migrate off the deprecated Bubble Tea mouse Type API (SA1019)
The mouse classification helpers checked both the current Button/Action pair and
the deprecated msg.Type / tea.Mouse* constants. Bubble Tea's parser always
populates Button+Action and only DERIVES Type from them, so the Type fallbacks
were pure redundancy (a left-button drag is Action==Motion, already covered by
mouseMotion). Drop them — clears all 11 SA1019 findings in internal/tui with no
behavior change. The left-drag test now asserts the Action==Motion path directly.

Gates: gofmt/vet/build(host+linux+windows)/test -race/staticcheck(SA1019 0)/
deadcode(no new) all pass.
2026-06-15 19:08:26 +05:30
6 changed files with 11 additions and 133 deletions

No files matched your search

-6
View File
@@ -367,12 +367,6 @@ func runExec(args []string, stdout io.Writer, stderr io.Writer, deps appDeps) in
Mode: notify.Mode(strings.TrimSpace(execNotifyMode(options, resolved))),
FocusMode: notify.FocusAlways,
})
// Opt-in webhook fan-out (ZERO_NOTIFY_WEBHOOK_URL). Headless runs can safely
// log a failed delivery to stderr (never stdout). The sink redacts before
// logging, so a token in the URL or message is masked.
notify.MaybeAddWebhookSink(notifier, os.Getenv, func(format string, args ...any) {
fmt.Fprintf(stderr, "[notify] "+format+"\n", args...)
})
if options.useSpec {
return runExecSpecDraft(execSpecDraftRun{
options: options,
-40
View File
@@ -1,40 +0,0 @@
package notify
import "strings"
// Webhook delivery is configured entirely from the environment. A webhook URL
// typically embeds a secret token, so sourcing it from the environment keeps it
// out of any on-disk config file. The sink is strictly opt-in: with
// EnvWebhookURL unset the wiring helper attaches nothing and the notifier
// behaves exactly as before.
const (
// EnvWebhookURL holds the destination webhook/Slack URL. Empty disables it.
EnvWebhookURL = "ZERO_NOTIFY_WEBHOOK_URL"
// EnvWebhookSummary is an optional one-line run summary attached to every
// payload (for example "nightly audit run").
EnvWebhookSummary = "ZERO_NOTIFY_WEBHOOK_SUMMARY"
)
// MaybeAddWebhookSink attaches a webhook sink to n when a webhook URL is present
// in the environment, and is otherwise a no-op. env resolves an environment
// variable (pass os.Getenv); logf records one redacted line per failed delivery
// (pass nil to stay silent — for example a TUI that owns the screen). It is safe
// to call unconditionally: configuration alone decides whether the sink exists.
//
// The attached sink is still subject to the notifier's Mode/focus policy, so a
// webhook only delivers when notifications are enabled (for example
// `--notify both`), matching the rest of the notification surface.
func MaybeAddWebhookSink(n *Notifier, env func(string) string, logf func(format string, args ...any)) {
if n == nil || env == nil {
return
}
url := strings.TrimSpace(env(EnvWebhookURL))
if url == "" {
return
}
n.AddSink(NewWebhookSink(WebhookConfig{
URL: url,
Summary: strings.TrimSpace(env(EnvWebhookSummary)),
Logf: logf,
}))
}
-71
View File
@@ -1,71 +0,0 @@
package notify
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
)
// envFunc builds a deterministic env resolver from a map for the wiring tests.
func envFunc(values map[string]string) func(string) string {
return func(key string) string { return values[key] }
}
func TestMaybeAddWebhookSinkAttachesAndDelivers(t *testing.T) {
var hits int32
var gotSummary string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&hits, 1)
var payload webhookPayload
_ = json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&payload)
gotSummary = payload.Summary
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
n := New(&bytes.Buffer{}, Config{Mode: ModeBell, FocusMode: FocusAlways})
MaybeAddWebhookSink(n, envFunc(map[string]string{
EnvWebhookURL: server.URL,
EnvWebhookSummary: "nightly audit",
}), nil)
if got := len(n.sinks); got != 1 {
t.Fatalf("expected 1 sink attached, got %d", got)
}
n.Notify(Completion, "Zero: ready")
if got := atomic.LoadInt32(&hits); got != 1 {
t.Fatalf("webhook hit %d times, want 1", got)
}
if gotSummary != "nightly audit" {
t.Fatalf("summary = %q, want %q", gotSummary, "nightly audit")
}
}
func TestMaybeAddWebhookSinkNoopWhenURLBlank(t *testing.T) {
n := New(&bytes.Buffer{}, Config{Mode: ModeBell, FocusMode: FocusAlways})
// Unset.
MaybeAddWebhookSink(n, envFunc(nil), nil)
// Set but blank / whitespace only.
MaybeAddWebhookSink(n, envFunc(map[string]string{EnvWebhookURL: " "}), nil)
if got := len(n.sinks); got != 0 {
t.Fatalf("expected no sink attached, got %d", got)
}
}
func TestMaybeAddWebhookSinkNilGuards(t *testing.T) {
// Must not panic on a nil notifier or nil env resolver.
MaybeAddWebhookSink(nil, envFunc(map[string]string{EnvWebhookURL: "https://example.test"}), nil)
n := New(&bytes.Buffer{}, Config{Mode: ModeBell, FocusMode: FocusAlways})
MaybeAddWebhookSink(n, nil, nil)
if got := len(n.sinks); got != 0 {
t.Fatalf("nil env must attach nothing, got %d sinks", got)
}
}
-4
View File
@@ -365,10 +365,6 @@ func newModel(ctx context.Context, options Options) model {
Mode: notify.Mode(strings.TrimSpace(options.Notify.Mode)),
FocusMode: notify.FocusMode(strings.TrimSpace(options.Notify.FocusMode)),
})
// Opt-in webhook fan-out (ZERO_NOTIFY_WEBHOOK_URL). Delivery failures stay
// silent here: the TUI owns the alt-screen, so writing to stderr would
// corrupt the display.
notify.MaybeAddWebhookSink(notifier, os.Getenv, nil)
notifier.SetFocused(true)
m := model{
+9 -9
View File
@@ -170,28 +170,28 @@ func (m model) syncMouseCapture() (model, tea.Cmd) {
return m, tea.DisableMouse
}
// Bubble Tea's Type field is deprecated, but its parser still populates it for
// compatibility cases such as left-button drag events. Keep these helpers
// tolerant of both the current Button/Action pair and legacy Type values.
// Mouse classification uses the current Button/Action pair only. Bubble Tea's
// parser always populates Button+Action and merely derives the deprecated Type
// field from them, so checking Type adds nothing — and a left-button drag is
// Action==Motion (which mouseMotion already covers), not a press.
func mouseLeftPress(msg tea.MouseMsg) bool {
return msg.Button == tea.MouseButtonLeft && msg.Action == tea.MouseActionPress ||
msg.Type == tea.MouseLeft && msg.Action == tea.MouseActionPress
return msg.Button == tea.MouseButtonLeft && msg.Action == tea.MouseActionPress
}
func mouseMotion(msg tea.MouseMsg) bool {
return msg.Action == tea.MouseActionMotion || msg.Type == tea.MouseMotion
return msg.Action == tea.MouseActionMotion
}
func mouseRelease(msg tea.MouseMsg) bool {
return msg.Action == tea.MouseActionRelease || msg.Type == tea.MouseRelease
return msg.Action == tea.MouseActionRelease
}
func mouseWheelUp(msg tea.MouseMsg) bool {
return msg.Button == tea.MouseButtonWheelUp || msg.Type == tea.MouseWheelUp
return msg.Button == tea.MouseButtonWheelUp
}
func mouseWheelDown(msg tea.MouseMsg) bool {
return msg.Button == tea.MouseButtonWheelDown || msg.Type == tea.MouseWheelDown
return msg.Button == tea.MouseButtonWheelDown
}
func (m model) mouseOverComposer(msg tea.MouseMsg) bool {
+2 -3
View File
@@ -348,12 +348,11 @@ func TestTranscriptSelectionLeftDragDoesNotResetAnchor(t *testing.T) {
Y: 1,
})
m = updated.(model)
// Bubble Tea marks left-button drag motion as Type MouseLeft for backward
// compatibility; this must update the cursor without resetting the anchor.
// A left-button drag is Action==Motion with Button==Left; this must update the
// cursor without resetting the selection anchor.
updated, _ = m.Update(tea.MouseMsg{
Button: tea.MouseButtonLeft,
Action: tea.MouseActionMotion,
Type: tea.MouseLeft,
X: 8,
Y: 1,
})