- handleHook validates POST method (405 on others) - MaxBytesReader limits body to 64KB (400 on overflow) - JSON decode errors return 400 - Valid payloads update registry via processHookEvent - 4 tests cover OK, method not allowed, bad JSON, body too large Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
303 lines
7.2 KiB
Go
303 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestProcessHookNotificationPermission(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
d.processHookEvent(HookEvent{
|
|
SessionID: "sess-1",
|
|
HookEventName: "Notification",
|
|
NotificationType: "permission_prompt",
|
|
Cwd: "/home/user/project",
|
|
})
|
|
|
|
list := d.registry.List()
|
|
if len(list) != 1 {
|
|
t.Fatalf("registry len = %d, want 1", len(list))
|
|
}
|
|
if list[0].State != "Needs Input" {
|
|
t.Errorf("State = %q, want %q", list[0].State, "Needs Input")
|
|
}
|
|
if list[0].WaitType != "permission" {
|
|
t.Errorf("WaitType = %q, want %q", list[0].WaitType, "permission")
|
|
}
|
|
}
|
|
|
|
func TestProcessHookNotificationIdle(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
d.processHookEvent(HookEvent{
|
|
SessionID: "sess-1",
|
|
HookEventName: "Notification",
|
|
NotificationType: "idle_prompt",
|
|
Cwd: "/home/user/project",
|
|
})
|
|
|
|
list := d.registry.List()
|
|
if len(list) != 1 {
|
|
t.Fatalf("registry len = %d, want 1", len(list))
|
|
}
|
|
if list[0].State != "Needs Input" {
|
|
t.Errorf("State = %q, want %q", list[0].State, "Needs Input")
|
|
}
|
|
if list[0].WaitType != "idle" {
|
|
t.Errorf("WaitType = %q, want %q", list[0].WaitType, "idle")
|
|
}
|
|
}
|
|
|
|
func TestProcessHookNotificationUnknown(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
d.processHookEvent(HookEvent{
|
|
SessionID: "sess-1",
|
|
HookEventName: "Notification",
|
|
NotificationType: "some_unknown_type",
|
|
Cwd: "/home/user/project",
|
|
})
|
|
|
|
list := d.registry.List()
|
|
if len(list) != 1 {
|
|
t.Fatalf("registry len = %d, want 1", len(list))
|
|
}
|
|
if list[0].State != "Needs Input" {
|
|
t.Errorf("State = %q, want %q", list[0].State, "Needs Input")
|
|
}
|
|
if list[0].WaitType != "question" {
|
|
t.Errorf("WaitType = %q, want %q", list[0].WaitType, "question")
|
|
}
|
|
}
|
|
|
|
func TestProcessHookStop(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
d.processHookEvent(HookEvent{
|
|
SessionID: "sess-1",
|
|
HookEventName: "Stop",
|
|
Cwd: "/home/user/project",
|
|
})
|
|
|
|
list := d.registry.List()
|
|
if len(list) != 1 {
|
|
t.Fatalf("registry len = %d, want 1", len(list))
|
|
}
|
|
if list[0].State != "Needs Input" {
|
|
t.Errorf("State = %q, want %q", list[0].State, "Needs Input")
|
|
}
|
|
if list[0].WaitType != "question" {
|
|
t.Errorf("WaitType = %q, want %q", list[0].WaitType, "question")
|
|
}
|
|
}
|
|
|
|
func TestProcessHookPostToolUse(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
// First set session to Needs Input
|
|
d.registry.UpdateFromHook("sess-1", "Needs Input", "permission", "/tmp")
|
|
|
|
d.processHookEvent(HookEvent{
|
|
SessionID: "sess-1",
|
|
HookEventName: "PostToolUse",
|
|
ToolName: "Bash",
|
|
Cwd: "/home/user/project",
|
|
})
|
|
|
|
list := d.registry.List()
|
|
if len(list) != 1 {
|
|
t.Fatalf("registry len = %d, want 1", len(list))
|
|
}
|
|
if list[0].State != "Working" {
|
|
t.Errorf("State = %q, want %q", list[0].State, "Working")
|
|
}
|
|
if list[0].WaitType != "" {
|
|
t.Errorf("WaitType = %q, want empty", list[0].WaitType)
|
|
}
|
|
}
|
|
|
|
func TestProcessHookPreToolUse(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
d.processHookEvent(HookEvent{
|
|
SessionID: "sess-1",
|
|
HookEventName: "PreToolUse",
|
|
ToolName: "Read",
|
|
Cwd: "/home/user/project",
|
|
})
|
|
|
|
list := d.registry.List()
|
|
if len(list) != 1 {
|
|
t.Fatalf("registry len = %d, want 1", len(list))
|
|
}
|
|
if list[0].State != "Working" {
|
|
t.Errorf("State = %q, want %q", list[0].State, "Working")
|
|
}
|
|
if list[0].WaitType != "" {
|
|
t.Errorf("WaitType = %q, want empty", list[0].WaitType)
|
|
}
|
|
}
|
|
|
|
func TestProcessHookIgnoresEmptySessionID(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
d.processHookEvent(HookEvent{
|
|
SessionID: "",
|
|
HookEventName: "Notification",
|
|
})
|
|
|
|
list := d.registry.List()
|
|
if len(list) != 0 {
|
|
t.Errorf("registry len = %d, want 0 (empty session_id should be ignored)", len(list))
|
|
}
|
|
}
|
|
|
|
func TestProcessHookIgnoresUnknownEvent(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
d.processHookEvent(HookEvent{
|
|
SessionID: "sess-1",
|
|
HookEventName: "SomeUnknownEvent",
|
|
})
|
|
|
|
list := d.registry.List()
|
|
if len(list) != 0 {
|
|
t.Errorf("registry len = %d, want 0 (unknown event should be ignored)", len(list))
|
|
}
|
|
}
|
|
|
|
func TestUpdateFromHookCreatesNewEntry(t *testing.T) {
|
|
reg := NewRegistry()
|
|
|
|
reg.UpdateFromHook("new-sess", "Working", "", "/home/user/project")
|
|
|
|
list := reg.List()
|
|
if len(list) != 1 {
|
|
t.Fatalf("registry len = %d, want 1", len(list))
|
|
}
|
|
if list[0].SessionID != "new-sess" {
|
|
t.Errorf("SessionID = %q, want %q", list[0].SessionID, "new-sess")
|
|
}
|
|
if list[0].State != "Working" {
|
|
t.Errorf("State = %q, want %q", list[0].State, "Working")
|
|
}
|
|
if list[0].Cwd != "/home/user/project" {
|
|
t.Errorf("Cwd = %q, want %q", list[0].Cwd, "/home/user/project")
|
|
}
|
|
}
|
|
|
|
func TestUpdateFromHookSetsWaitingSince(t *testing.T) {
|
|
reg := NewRegistry()
|
|
|
|
// Start as Working
|
|
reg.UpdateFromHook("sess-1", "Working", "", "/tmp")
|
|
|
|
list := reg.List()
|
|
if list[0].WaitingSince != nil {
|
|
t.Error("WaitingSince should be nil when Working")
|
|
}
|
|
|
|
// Transition to Needs Input
|
|
reg.UpdateFromHook("sess-1", "Needs Input", "permission", "/tmp")
|
|
|
|
list = reg.List()
|
|
if list[0].WaitingSince == nil {
|
|
t.Fatal("WaitingSince should be set when transitioning to Needs Input")
|
|
}
|
|
}
|
|
|
|
func TestUpdateFromHookClearsWaitingSince(t *testing.T) {
|
|
reg := NewRegistry()
|
|
|
|
// Set up as Needs Input
|
|
reg.UpdateFromHook("sess-1", "Needs Input", "permission", "/tmp")
|
|
|
|
list := reg.List()
|
|
if list[0].WaitingSince == nil {
|
|
t.Fatal("WaitingSince should be set")
|
|
}
|
|
|
|
// Transition back to Working
|
|
reg.UpdateFromHook("sess-1", "Working", "", "/tmp")
|
|
|
|
list = reg.List()
|
|
if list[0].WaitingSince != nil {
|
|
t.Errorf("WaitingSince should be nil after returning to Working, got %v", list[0].WaitingSince)
|
|
}
|
|
}
|
|
|
|
func TestHandleHookPostOK(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
body := `{
|
|
"session_id": "sess-1",
|
|
"hook_event_name": "Notification",
|
|
"notification_type": "permission_prompt",
|
|
"cwd": "/home/user/project"
|
|
}`
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/hook", strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
|
|
d.handleHook(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
}
|
|
|
|
list := d.registry.List()
|
|
if len(list) != 1 {
|
|
t.Fatalf("registry len = %d, want 1", len(list))
|
|
}
|
|
if list[0].WaitType != "permission" {
|
|
t.Errorf("WaitType = %q, want %q", list[0].WaitType, "permission")
|
|
}
|
|
if list[0].State != "Needs Input" {
|
|
t.Errorf("State = %q, want %q", list[0].State, "Needs Input")
|
|
}
|
|
}
|
|
|
|
func TestHandleHookMethodNotAllowed(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/hook", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
d.handleHook(w, req)
|
|
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("status = %d, want 405", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleHookBadJSON(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/hook", strings.NewReader("not json"))
|
|
w := httptest.NewRecorder()
|
|
|
|
d.handleHook(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want 400", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestHandleHookBodyTooLarge(t *testing.T) {
|
|
d := newTestDaemon(t)
|
|
|
|
// 65KB body exceeds the 64KB limit
|
|
bigBody := strings.Repeat("x", 65*1024)
|
|
req := httptest.NewRequest(http.MethodPost, "/hook", strings.NewReader(bigBody))
|
|
w := httptest.NewRecorder()
|
|
|
|
d.handleHook(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("status = %d, want 400", w.Code)
|
|
}
|
|
}
|