diff --git a/daemon_test.go b/daemon_test.go index 757823e..a894ad2 100644 --- a/daemon_test.go +++ b/daemon_test.go @@ -2,9 +2,13 @@ package main import ( "encoding/json" + "fmt" + "io" "net" + "net/http" "os" "path/filepath" + "strings" "testing" "time" ) @@ -212,7 +216,128 @@ func newTestDaemon(t *testing.T) *Daemon { t.Fatalf("labels: %v", err) } - return NewDaemon(sockPath, procDir, claudeDir, labels) + d := NewDaemon(sockPath, procDir, claudeDir, labels) + d.hookPort = 0 // disable hook server by default in tests + return d +} + +func TestHookServerStartsWithDaemon(t *testing.T) { + d := newTestDaemon(t) + + // Assign a dynamic port for the hook server + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + port := ln.Addr().(*net.TCPAddr).Port + ln.Close() + + d.hookPort = port + + if err := d.Start(); err != nil { + t.Fatalf("start: %v", err) + } + defer d.Stop() + + // POST to /hook should return 200 + url := fmt.Sprintf("http://127.0.0.1:%d/hook", port) + resp, err := http.Post(url, "application/json", nil) + if err != nil { + t.Fatalf("POST /hook: %v", err) + } + defer resp.Body.Close() + // Empty body -> 400 (bad JSON), but the server is alive + // A valid POST should return 200 + body := `{"session_id":"test","hook_event_name":"Stop","cwd":"/tmp"}` + resp2, err := http.Post(url, "application/json", io.NopCloser( + io.Reader(strings.NewReader(body)), + )) + if err != nil { + t.Fatalf("POST /hook valid: %v", err) + } + defer resp2.Body.Close() + if resp2.StatusCode != http.StatusOK { + t.Errorf("status = %d, want 200", resp2.StatusCode) + } +} + +func TestHookServerStopsWithDaemon(t *testing.T) { + d := newTestDaemon(t) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + port := ln.Addr().(*net.TCPAddr).Port + ln.Close() + + d.hookPort = port + + if err := d.Start(); err != nil { + t.Fatalf("start: %v", err) + } + + d.Stop() + + // Give a moment for shutdown + time.Sleep(50 * time.Millisecond) + + // Port should no longer be listening + url := fmt.Sprintf("http://127.0.0.1:%d/hook", port) + _, err = http.Post(url, "application/json", nil) + if err == nil { + t.Error("expected connection error after Stop, but POST succeeded") + } +} + +func TestHookServerPortBusy(t *testing.T) { + d := newTestDaemon(t) + + // Occupy a port + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + defer ln.Close() + port := ln.Addr().(*net.TCPAddr).Port + + d.hookPort = port + + // Start should succeed despite port being busy (graceful degradation) + if err := d.Start(); err != nil { + t.Fatalf("start should succeed even with port busy: %v", err) + } + defer d.Stop() +} + +func TestPollSlowdown(t *testing.T) { + d := newTestDaemon(t) + + // No hook received -> default 5s + got := d.currentPollInterval() + if got != 5*time.Second { + t.Errorf("default poll = %v, want 5s", got) + } + + // Simulate recent hook + d.mu.Lock() + d.lastHookTime = time.Now() + d.mu.Unlock() + + got = d.currentPollInterval() + if got != 20*time.Second { + t.Errorf("after hook poll = %v, want 20s", got) + } + + // Simulate hook > 60s ago + d.mu.Lock() + d.lastHookTime = time.Now().Add(-61 * time.Second) + d.mu.Unlock() + + got = d.currentPollInterval() + if got != 5*time.Second { + t.Errorf("after 60s poll = %v, want 5s", got) + } } func TestDaemonStartStop(t *testing.T) {