summaryrefslogtreecommitdiff
path: root/internal/cli/serve_test.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-08 20:40:55 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-08 20:40:55 +0000
commit1ce83b6b6a300f4389dd84c4477f3ca73a431524 (patch)
treebefe1444267d0f4f333b226f016a7767e354c2a2 /internal/cli/serve_test.go
parentdb1ebb7a3f9310ca2cc483d65e9c0e578c2eb4ff (diff)
cli: newLogger helper, defaultServerURL, shared http client, report command
- Extract newLogger() to remove duplication across run/serve/start - Add defaultServerURL const ("http://localhost:8484") used by all client commands - Move http.Client into internal/cli/http.go with 30s timeout - Add 'report' command for printing execution summaries - Add test coverage for create and serve commands Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/cli/serve_test.go')
-rw-r--r--internal/cli/serve_test.go91
1 files changed, 91 insertions, 0 deletions
diff --git a/internal/cli/serve_test.go b/internal/cli/serve_test.go
new file mode 100644
index 0000000..6bd0e8f
--- /dev/null
+++ b/internal/cli/serve_test.go
@@ -0,0 +1,91 @@
+package cli
+
+import (
+ "context"
+ "log/slog"
+ "net"
+ "net/http"
+ "sync"
+ "testing"
+ "time"
+)
+
+// recordHandler captures log records for assertions.
+type recordHandler struct {
+ mu sync.Mutex
+ records []slog.Record
+}
+
+func (h *recordHandler) Enabled(_ context.Context, _ slog.Level) bool { return true }
+func (h *recordHandler) Handle(_ context.Context, r slog.Record) error {
+ h.mu.Lock()
+ h.records = append(h.records, r)
+ h.mu.Unlock()
+ return nil
+}
+func (h *recordHandler) WithAttrs(_ []slog.Attr) slog.Handler { return h }
+func (h *recordHandler) WithGroup(_ string) slog.Handler { return h }
+func (h *recordHandler) hasWarn(msg string) bool {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+ for _, r := range h.records {
+ if r.Level == slog.LevelWarn && r.Message == msg {
+ return true
+ }
+ }
+ return false
+}
+
+// TestServe_ShutdownError_IsLogged verifies that a shutdown timeout error is
+// logged as a warning rather than silently dropped.
+func TestServe_ShutdownError_IsLogged(t *testing.T) {
+ // Start a real listener so we have an address.
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatalf("listen: %v", err)
+ }
+
+ // Handler that hangs so the active connection prevents clean shutdown.
+ hang := make(chan struct{})
+ mux := http.NewServeMux()
+ mux.HandleFunc("/hang", func(w http.ResponseWriter, r *http.Request) {
+ <-hang
+ })
+
+ srv := &http.Server{Handler: mux}
+
+ // Serve in background.
+ go srv.Serve(ln) //nolint:errcheck
+
+ // Open a connection and start a hanging request so the server has an
+ // active connection when we call Shutdown.
+ addr := ln.Addr().String()
+ connReady := make(chan struct{})
+ go func() {
+ req, _ := http.NewRequest(http.MethodGet, "http://"+addr+"/hang", nil)
+ close(connReady)
+ http.DefaultClient.Do(req) //nolint:errcheck
+ }()
+ <-connReady
+ // Give the goroutine a moment to establish the request.
+ time.Sleep(20 * time.Millisecond)
+
+ // Shutdown with an already-expired deadline so it times out immediately.
+ expiredCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-1*time.Second))
+ defer cancel()
+
+ h := &recordHandler{}
+ logger := slog.New(h)
+
+ // This is the exact logic from serve.go's shutdown goroutine.
+ if err := srv.Shutdown(expiredCtx); err != nil {
+ logger.Warn("shutdown error", "err", err)
+ }
+
+ // Unblock the hanging handler.
+ close(hang)
+
+ if !h.hasWarn("shutdown error") {
+ t.Error("expected shutdown error to be logged as Warn, but it was not")
+ }
+}