TestChatIdleTimeoutAbortsStalledStream reproduces the exact hang: the server returns 200 OK then sends nothing. Without the idle watchdog scanner.Scan() blocks forever; with it, the body is closed and the turn ends in an EventError naming the stall, a finite escape that doesn't need Ctrl+C.
(t *testing.T)
| 864 | defer srv.Close() |
| 865 | |
| 866 | c := New(srv.URL, "some-model", "") |
| 867 | for _, e := range collect(c.Chat(context.Background(), nil, nil)) { |
| 868 | _ = e // the turn errors (the 400 is real), that's expected |
| 869 | } |
| 870 | if len(bodies) != 1 { |
| 871 | t.Fatalf("unrelated 400 must NOT trigger a fallback retry; got %d requests", len(bodies)) |
| 872 | } |
| 873 | if c.noReasoningEffort.Load() { |
| 874 | t.Fatal("reasoning must not latch off on a 400 unrelated to reasoning") |
| 875 | } |
| 876 | } |
| 877 | |
| 878 | // TestNewHasNoHTTPTimeout pins that the streaming Client must NOT set |
| 879 | // http.Client.Timeout: that field is end-to-end (it covers body reads) and would |
| 880 | // abort a legitimately slow SSE stream with "context deadline exceeded … while |
| 881 | // reading body" on slow local backends. Per-turn context cancellation governs |
| 882 | // request lifetime; this stops a refactor from reintroducing the wall-clock cap. |
| 883 | func TestNewHasNoHTTPTimeout(t *testing.T) { |
| 884 | c := New("http://example.test", "model", "token") |
| 885 | if c.HTTP.Timeout != 0 { |
| 886 | t.Fatalf("http.Client.Timeout must be 0 so per-turn context governs SSE lifetime; got %v", c.HTTP.Timeout) |
| 887 | } |
| 888 | } |
| 889 | |
| 890 | // TestIdleTimeoutFromEnv pins the CODEHAMR_IDLE_TIMEOUT contract: a Go duration |
| 891 | // or bare-seconds string wins, anything else (unset, garbage, non-positive) |
| 892 | // falls back to the default. The default is deliberately generous because this |
| 893 | // is a dead-connection detector, not a loop guard. |
| 894 | func TestIdleTimeoutFromEnv(t *testing.T) { |
| 895 | cases := []struct { |
| 896 | val string |
| 897 | set bool |