mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 17:56:14 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b7b8296ac |
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user