TestClassifyHTTPStatus covers the HTTP status → ErrorCode mapping. Each documented status maps to the expected code; unmapped statuses fall through to server_error.
(t *testing.T)
| 156 | // Each documented status maps to the expected code; unmapped statuses |
| 157 | // fall through to server_error. |
| 158 | func TestClassifyHTTPStatus(t *testing.T) { |
| 159 | cases := []struct { |
| 160 | name string |
| 161 | status int |
| 162 | body string |
| 163 | wantCode ErrorCode |
| 164 | }{ |
| 165 | {"401_unauthorized", http.StatusUnauthorized, "token invalid", ErrAuthRequired}, |
| 166 | {"403_forbidden", http.StatusForbidden, "role insufficient", ErrPermissionDenied}, |
| 167 | {"404_item", http.StatusNotFound, "item TASK-99 not found", ErrItemNotFound}, |
| 168 | {"404_workspace", http.StatusNotFound, "workspace foo not visible", ErrUnknownWorkspace}, |
| 169 | {"409_conflict", http.StatusConflict, "version mismatch", ErrConflict}, |
| 170 | {"422_validation", http.StatusUnprocessableEntity, "title required", ErrValidationFailed}, |
| 171 | {"400_validation", http.StatusBadRequest, "bad input", ErrValidationFailed}, |
| 172 | // TASK-1078: 5xx now maps to upstream_error (distinct from |
| 173 | // server_error, which is reserved for dispatcher internal |
| 174 | // failures + un-mapped 4xx). Pre-fix every 5xx collapsed to |
| 175 | // ErrServerError; the new code lets agents distinguish |
| 176 | // "backend hiccup, retry" from "dispatcher bug, escalate." |
| 177 | {"500_upstream", http.StatusInternalServerError, "boom", ErrUpstreamError}, |
| 178 | {"503_upstream", http.StatusServiceUnavailable, "down", ErrUpstreamError}, |
| 179 | // BUG-1430: 429 is now a first-class ErrRateLimited code, |
| 180 | // distinct from the generic ErrServerError "other 4xx" bucket |
| 181 | // it landed in pre-fix. Agents implementing backoff key off |
| 182 | // the code without parsing free-form text. |
| 183 | {"429_rate_limited", http.StatusTooManyRequests, "rate_limited", ErrRateLimited}, |
| 184 | {"418_other", http.StatusTeapot, "weird", ErrServerError}, |
| 185 | } |
| 186 | for _, tc := range cases { |
| 187 | t.Run(tc.name, func(t *testing.T) { |
| 188 | res := classifyHTTPStatus(context.Background(), |
| 189 | "pad item list", tc.status, []byte(tc.body), nil) |
| 190 | if !res.IsError { |
| 191 | t.Errorf("expected IsError") |
| 192 | } |
| 193 | env := decodeEnvelope(t, res) |
| 194 | if env.Error.Code != tc.wantCode { |
| 195 | t.Errorf("Code = %q, want %q", env.Error.Code, tc.wantCode) |
| 196 | } |
| 197 | // Body fragment preservation contract changed in TASK-1077: |
| 198 | // pre-fix the raw body was always copied into Hint, which |
| 199 | // would leak unstructured upstream output (Codex review #387 |
| 200 | // round 1). Post-fix only structured-envelope-shaped bodies |
| 201 | // have their inner message lifted; unstructured bodies are |
| 202 | // dropped to avoid leaking tokens / debug dumps. |
| 203 | // |
| 204 | // Each test case above passes a bare string body — those |
| 205 | // hit the safe-fallback path and don't appear in the |
| 206 | // envelope. The unknown_workspace branch can also legitimately |
| 207 | // emit an empty Hint (no upstream message AND no extractable |
| 208 | // slug AND no lister wired). This test asserts the code |
| 209 | // mapping; body preservation specifically is tested in |
| 210 | // TestExtractUpstreamMessage with structured input, and hint |
| 211 | // shape per code is tested in |
| 212 | // TestClassifyHTTPStatus_HintsAreActionable. |
| 213 | _ = env.Error.Hint |
| 214 | }) |
| 215 | } |
nothing calls this directly
no test coverage detected