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) } }