Files
agent-dashboard/internal/hook/hook_test.go
2026-06-02 14:35:30 +08:00

225 lines
6.0 KiB
Go

package hook
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/longbin/agent-notify/internal/config"
"github.com/longbin/agent-notify/internal/inbox"
"github.com/longbin/agent-notify/internal/remote"
)
func TestCursorStopHookDisabled(t *testing.T) {
stubCursorSend(t)
cfg := config.Default()
cfg.Events.Stop = false
err := RunCursor(bytes.NewReader([]byte(`{"workspace_roots":["/tmp/proj"]}`)), cfg, "stop", &bytes.Buffer{})
if err != nil {
t.Fatal(err)
}
}
func TestClaudeStopOutputsTerminalSequence(t *testing.T) {
cfg := config.Default()
var out bytes.Buffer
err := RunClaude(strings.NewReader(`{"stop_hook_active":false}`), cfg, "stop", &out)
if err != nil {
t.Fatal(err)
}
var resp map[string]string
if err := json.Unmarshal(out.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if !strings.Contains(resp["terminalSequence"], "777;notify") {
t.Fatalf("bad sequence: %v", resp)
}
}
func TestClaudeStopHookActiveSkips(t *testing.T) {
cfg := config.Default()
var out bytes.Buffer
err := RunClaude(strings.NewReader(`{"stop_hook_active":true}`), cfg, "stop", &out)
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(out.String()) != "{}" {
t.Fatalf("expected {}, got %q", out.String())
}
}
func TestCursorHookUploadsInboxRecord(t *testing.T) {
stubCursorSend(t)
var uploaded inbox.Record
stubInbox(t,
func(rec inbox.Record, cfg config.Config) error {
uploaded = rec
return nil
},
func(rec inbox.Record) error {
t.Fatalf("unexpected fallback append: %+v", rec)
return nil
},
)
cfg := config.Default()
cfg.Inbox.Enabled = true
err := RunCursor(bytes.NewReader([]byte(`{"workspace_roots":["/tmp/proj"]}`)), cfg, "stop", &bytes.Buffer{})
if err != nil {
t.Fatal(err)
}
if uploaded.Agent != "Cursor" || uploaded.Event != "stop" || uploaded.CWD != "/tmp/proj" {
t.Fatalf("unexpected upload record: %+v", uploaded)
}
}
func TestCursorHookFallbacksWhenInboxUploadFails(t *testing.T) {
stubCursorSend(t)
var fallback inbox.Record
stubInbox(t,
func(rec inbox.Record, cfg config.Config) error {
return errors.New("offline")
},
func(rec inbox.Record) error {
fallback = rec
return nil
},
)
cfg := config.Default()
cfg.Inbox.Enabled = true
err := RunCursor(bytes.NewReader([]byte(`{"workspace_roots":["/tmp/proj"]}`)), cfg, "stop", &bytes.Buffer{})
if err != nil {
t.Fatal(err)
}
if fallback.Source != inbox.SourceFallback || fallback.Title == "" {
t.Fatalf("unexpected fallback record: %+v", fallback)
}
}
func TestDisabledHookDoesNotRecordInbox(t *testing.T) {
stubCursorSend(t)
stubInbox(t,
func(rec inbox.Record, cfg config.Config) error {
t.Fatalf("unexpected upload: %+v", rec)
return nil
},
func(rec inbox.Record) error {
t.Fatalf("unexpected fallback: %+v", rec)
return nil
},
)
cfg := config.Default()
cfg.Events.Stop = false
if err := RunCursor(bytes.NewReader([]byte(`{}`)), cfg, "stop", &bytes.Buffer{}); err != nil {
t.Fatal(err)
}
}
func TestClaudeHookUploadsInboxRecord(t *testing.T) {
var uploaded inbox.Record
stubInbox(t,
func(rec inbox.Record, cfg config.Config) error {
uploaded = rec
return nil
},
func(rec inbox.Record) error {
t.Fatalf("unexpected fallback append: %+v", rec)
return nil
},
)
cfg := config.Default()
cfg.Inbox.Enabled = true
var out bytes.Buffer
if err := RunClaude(strings.NewReader(`{"stop_hook_active":false}`), cfg, "stop", &out); err != nil {
t.Fatal(err)
}
if uploaded.Agent != "Claude" || uploaded.Event != "stop" || uploaded.Title == "" {
t.Fatalf("unexpected upload record: %+v", uploaded)
}
}
func stubInbox(t *testing.T, upload func(inbox.Record, config.Config) error, appendLocal func(inbox.Record) error) {
t.Helper()
prevUpload := uploadInboxRecord
prevAppend := appendInboxRecord
uploadInboxRecord = upload
appendInboxRecord = appendLocal
t.Cleanup(func() {
uploadInboxRecord = prevUpload
appendInboxRecord = prevAppend
})
}
func stubReportRemote(t *testing.T, fn func(config.Config, string, string, string, string, string)) {
t.Helper()
prev := reportRemoteHook
reportRemoteHook = fn
t.Cleanup(func() { reportRemoteHook = prev })
}
func TestCursorHookReportsRemoteOnStop(t *testing.T) {
stubCursorSend(t)
var got remote.StatusReport
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/status" {
http.NotFound(w, r)
return
}
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
http.Error(w, "decode", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
cfg := config.Default()
cfg.Remote.Enabled = true
cfg.Remote.URL = srv.URL
err := RunCursor(bytes.NewReader([]byte(`{"workspace_roots":["/tmp/proj"],"conversation_id":"conv-1","transcript_path":"/tmp/t.jsonl"}`)), cfg, "stop", &bytes.Buffer{})
if err != nil {
t.Fatal(err)
}
if got.Agent != "Cursor" || got.Event != "stop" || got.Status != "waiting" || got.CWD != "/tmp/proj" {
t.Fatalf("unexpected report: %+v", got)
}
if got.ConversationID != "conv-1" {
t.Fatalf("conversation_id: got %q", got.ConversationID)
}
}
func TestCursorHookSkipsRemoteWhenDisabled(t *testing.T) {
stubCursorSend(t)
stubReportRemote(t, func(config.Config, string, string, string, string, string) {
t.Fatal("reportRemote should not run when remote disabled")
})
cfg := config.Default()
cfg.Remote.Enabled = false
if err := RunCursor(bytes.NewReader([]byte(`{"workspace_roots":["/tmp/proj"]}`)), cfg, "stop", &bytes.Buffer{}); err != nil {
t.Fatal(err)
}
}
func TestCursorHookRemoteReportFailureStillOK(t *testing.T) {
stubCursorSend(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "fail", http.StatusInternalServerError)
}))
defer srv.Close()
cfg := config.Default()
cfg.Remote.Enabled = true
cfg.Remote.URL = srv.URL
if err := RunCursor(bytes.NewReader([]byte(`{"workspace_roots":["/tmp/proj"]}`)), cfg, "stop", &bytes.Buffer{}); err != nil {
t.Fatalf("hook should return nil on remote failure, got %v", err)
}
}