rdev/internal/adapter/claudebox/client_test.go
jordan d74efb75ff fix: wire workService to WorkersHandler and add /work/tasks endpoint
Critical fix: WorkersHandler was missing workService dependency, causing
500 errors when workers tried to fail tasks. This caused tasks to get
stuck in "running" state permanently.

Also adds:
- /work/tasks endpoint for debugging all tasks across projects
- List method to WorkQueue interface for admin views
- HTTP client tests for api_client.go and claudebox/client.go (48 tests)
- Split work.go DTOs into work_dto.go to stay under 500 lines

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 10:35:39 -07:00

775 lines
22 KiB
Go

package claudebox
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
)
func TestNewClient_DefaultTimeout(t *testing.T) {
client := NewClient(ClientConfig{
BaseURL: "http://localhost:8080",
})
if client.httpClient.Timeout != 10*time.Minute {
t.Errorf("expected default timeout 10m, got %v", client.httpClient.Timeout)
}
}
func TestNewClient_CustomTimeout(t *testing.T) {
client := NewClient(ClientConfig{
BaseURL: "http://localhost:8080",
Timeout: 5 * time.Minute,
})
if client.httpClient.Timeout != 5*time.Minute {
t.Errorf("expected timeout 5m, got %v", client.httpClient.Timeout)
}
}
func TestNewClient_TrimsTrailingSlash(t *testing.T) {
client := NewClient(ClientConfig{
BaseURL: "http://localhost:8080/",
})
if client.baseURL != "http://localhost:8080" {
t.Errorf("expected trailing slash trimmed, got %s", client.baseURL)
}
}
func TestHealth_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/health" {
t.Errorf("expected /health, got %s", r.URL.Path)
}
resp := HealthResponse{
Status: "healthy",
Timestamp: "2024-01-15T10:30:00Z",
WorkDir: "/workspace",
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
health, err := client.Health(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if health.Status != "healthy" {
t.Errorf("expected status 'healthy', got %s", health.Status)
}
if health.WorkDir != "/workspace" {
t.Errorf("expected work_dir '/workspace', got %s", health.WorkDir)
}
}
func TestHealth_Unhealthy(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
_, err := client.Health(context.Background())
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "health check returned status 503") {
t.Errorf("expected 503 error, got %v", err)
}
}
func TestHealth_NetworkError(t *testing.T) {
client := NewClient(ClientConfig{
BaseURL: "http://localhost:1",
Timeout: 100 * time.Millisecond,
})
_, err := client.Health(context.Background())
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "health check:") {
t.Errorf("expected error wrapped with 'health check:', got %v", err)
}
}
func TestExecute_Success(t *testing.T) {
var receivedReq ExecuteRequest
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/execute" {
t.Errorf("expected /execute, got %s", r.URL.Path)
}
if r.Header.Get("Content-Type") != "application/json" {
t.Errorf("expected Content-Type application/json, got %s", r.Header.Get("Content-Type"))
}
body, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(body, &receivedReq)
resp := ExecuteResponse{
Success: true,
Output: "Task completed",
ExitCode: 0,
DurationMs: 5000,
SessionID: "session-123",
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
req := &ExecuteRequest{
Prompt: "Build the project",
AllowedTools: []string{"Bash", "Read", "Write"},
WorkingDir: "/workspace/project",
Timeout: 300,
Metadata: map[string]string{"task_id": "task-1"},
}
result, err := client.Execute(context.Background(), req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.Success {
t.Error("expected success=true")
}
if result.Output != "Task completed" {
t.Errorf("expected output 'Task completed', got %s", result.Output)
}
if result.ExitCode != 0 {
t.Errorf("expected exit code 0, got %d", result.ExitCode)
}
// Verify request was serialized correctly
if receivedReq.Prompt != "Build the project" {
t.Errorf("expected prompt 'Build the project', got %s", receivedReq.Prompt)
}
if len(receivedReq.AllowedTools) != 3 {
t.Errorf("expected 3 allowed tools, got %d", len(receivedReq.AllowedTools))
}
}
func TestExecute_ErrorStatus(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"error":"invalid prompt"}`))
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
_, err := client.Execute(context.Background(), &ExecuteRequest{Prompt: ""})
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "execute returned status 400") {
t.Errorf("expected 400 error, got %v", err)
}
if !strings.Contains(err.Error(), "invalid prompt") {
t.Errorf("expected error body in message, got %v", err)
}
}
func TestExecute_MalformedResponse(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{invalid json`))
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
_, err := client.Execute(context.Background(), &ExecuteRequest{Prompt: "test"})
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "decode response") {
t.Errorf("expected decode error, got %v", err)
}
}
func TestExecuteStream_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/execute/stream" {
t.Errorf("expected /execute/stream, got %s", r.URL.Path)
}
if r.Header.Get("Accept") != "text/event-stream" {
t.Errorf("expected Accept text/event-stream, got %s", r.Header.Get("Accept"))
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, ok := w.(http.Flusher)
if !ok {
t.Fatal("expected http.Flusher")
}
events := []StreamEvent{
{Type: "start", Timestamp: "2024-01-15T10:30:00Z"},
{Type: "output", Content: "Building...", Timestamp: "2024-01-15T10:30:01Z"},
{Type: "tool_call", ToolName: "Bash", Timestamp: "2024-01-15T10:30:02Z"},
{Type: "complete", Content: "Done", Timestamp: "2024-01-15T10:30:05Z"},
}
for _, event := range events {
data, _ := json.Marshal(event)
fmt.Fprintf(w, "data: %s\n\n", data)
flusher.Flush()
}
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
var receivedEvents []StreamEvent
var mu sync.Mutex
err := client.ExecuteStream(context.Background(), &ExecuteRequest{Prompt: "build"}, func(event StreamEvent) {
mu.Lock()
receivedEvents = append(receivedEvents, event)
mu.Unlock()
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(receivedEvents) != 4 {
t.Fatalf("expected 4 events, got %d", len(receivedEvents))
}
if receivedEvents[0].Type != "start" {
t.Errorf("expected first event type 'start', got %s", receivedEvents[0].Type)
}
if receivedEvents[1].Content != "Building..." {
t.Errorf("expected second event content 'Building...', got %s", receivedEvents[1].Content)
}
if receivedEvents[2].ToolName != "Bash" {
t.Errorf("expected third event tool name 'Bash', got %s", receivedEvents[2].ToolName)
}
}
func TestExecuteStream_SkipsMalformedEvents(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
flusher, _ := w.(http.Flusher)
// Valid event
event1, _ := json.Marshal(StreamEvent{Type: "start"})
fmt.Fprintf(w, "data: %s\n\n", event1)
flusher.Flush()
// Malformed JSON - should be skipped
fmt.Fprintf(w, "data: {invalid json}\n\n")
flusher.Flush()
// Empty data - should be skipped
fmt.Fprintf(w, "data: \n\n")
flusher.Flush()
// Non-data line - should be skipped
fmt.Fprintf(w, "event: ping\n\n")
flusher.Flush()
// Valid event
event2, _ := json.Marshal(StreamEvent{Type: "complete"})
fmt.Fprintf(w, "data: %s\n\n", event2)
flusher.Flush()
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
var receivedEvents []StreamEvent
var mu sync.Mutex
err := client.ExecuteStream(context.Background(), &ExecuteRequest{Prompt: "test"}, func(event StreamEvent) {
mu.Lock()
receivedEvents = append(receivedEvents, event)
mu.Unlock()
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Should only receive the 2 valid events
if len(receivedEvents) != 2 {
t.Fatalf("expected 2 events (malformed skipped), got %d", len(receivedEvents))
}
if receivedEvents[0].Type != "start" {
t.Errorf("expected first event 'start', got %s", receivedEvents[0].Type)
}
if receivedEvents[1].Type != "complete" {
t.Errorf("expected second event 'complete', got %s", receivedEvents[1].Type)
}
}
func TestExecuteStream_ErrorStatus(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"error":"agent unavailable"}`))
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
err := client.ExecuteStream(context.Background(), &ExecuteRequest{Prompt: "test"}, func(event StreamEvent) {
t.Error("handler should not be called on error")
})
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "execute stream returned status 500") {
t.Errorf("expected 500 error, got %v", err)
}
}
func TestExecuteStream_ContextCanceledBeforeRequest(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("handler should not be called when context is already canceled")
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
// Cancel context before making request
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := client.ExecuteStream(ctx, &ExecuteRequest{Prompt: "test"}, func(event StreamEvent) {})
if err == nil {
t.Fatal("expected error, got nil")
}
// Should get a context canceled error
}
func TestGitClone_Success(t *testing.T) {
var receivedReq GitCloneRequest
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/git/clone" {
t.Errorf("expected /git/clone, got %s", r.URL.Path)
}
body, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(body, &receivedReq)
resp := GitCloneResponse{
Success: true,
Cloned: true,
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
result, err := client.GitClone(context.Background(), "https://github.com/example/repo.git", "/workspace")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.Success {
t.Error("expected success=true")
}
if !result.Cloned {
t.Error("expected cloned=true")
}
if receivedReq.CloneURL != "https://github.com/example/repo.git" {
t.Errorf("expected clone URL, got %s", receivedReq.CloneURL)
}
if receivedReq.WorkDir != "/workspace" {
t.Errorf("expected work dir '/workspace', got %s", receivedReq.WorkDir)
}
}
func TestGitClone_AlreadyExists(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := GitCloneResponse{
Success: true,
Cloned: false, // Already existed, just updated
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
result, err := client.GitClone(context.Background(), "https://github.com/example/repo.git", "/workspace")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.Success {
t.Error("expected success=true")
}
if result.Cloned {
t.Error("expected cloned=false for existing repo")
}
}
func TestGitClone_ErrorStatus(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"error":"invalid clone URL"}`))
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
_, err := client.GitClone(context.Background(), "invalid", "/workspace")
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "git clone returned status 400") {
t.Errorf("expected 400 error, got %v", err)
}
}
func TestGitCommitAndPush_Success(t *testing.T) {
var receivedReq GitCommitAndPushRequest
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/git/commit-and-push" {
t.Errorf("expected /git/commit-and-push, got %s", r.URL.Path)
}
body, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(body, &receivedReq)
resp := GitCommitAndPushResponse{
Success: true,
HasChanges: true,
CommitSHA: "abc123def456",
FilesChanged: []string{"main.go", "go.mod"},
Pushed: true,
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
result, err := client.GitCommitAndPush(context.Background(), "feat: add feature", true, "/workspace")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.Success {
t.Error("expected success=true")
}
if !result.HasChanges {
t.Error("expected has_changes=true")
}
if result.CommitSHA != "abc123def456" {
t.Errorf("expected commit SHA abc123def456, got %s", result.CommitSHA)
}
if !result.Pushed {
t.Error("expected pushed=true")
}
if len(result.FilesChanged) != 2 {
t.Errorf("expected 2 files changed, got %d", len(result.FilesChanged))
}
// Verify request
if receivedReq.Message != "feat: add feature" {
t.Errorf("expected message 'feat: add feature', got %s", receivedReq.Message)
}
if !receivedReq.Push {
t.Error("expected push=true in request")
}
}
func TestGitCommitAndPush_NoChanges(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := GitCommitAndPushResponse{
Success: true,
HasChanges: false,
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
result, err := client.GitCommitAndPush(context.Background(), "test", false, "/workspace")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.Success {
t.Error("expected success=true")
}
if result.HasChanges {
t.Error("expected has_changes=false")
}
if result.CommitSHA != "" {
t.Errorf("expected empty commit SHA, got %s", result.CommitSHA)
}
}
func TestGitCommitAndPush_ErrorStatus(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"error":"git push failed"}`))
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
_, err := client.GitCommitAndPush(context.Background(), "test", true, "/workspace")
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "git commit returned status 500") {
t.Errorf("expected 500 error, got %v", err)
}
}
func TestGitStatus_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/git/status" {
t.Errorf("expected /git/status, got %s", r.URL.Path)
}
if r.URL.Query().Get("work_dir") != "/workspace/project" {
t.Errorf("expected work_dir query param, got %s", r.URL.Query().Get("work_dir"))
}
resp := GitStatusResponse{
IsRepo: true,
HasChanges: true,
ChangedFiles: []string{"main.go", "README.md"},
Branch: "feature/test",
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
result, err := client.GitStatus(context.Background(), "/workspace/project")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.IsRepo {
t.Error("expected is_repo=true")
}
if !result.HasChanges {
t.Error("expected has_changes=true")
}
if result.Branch != "feature/test" {
t.Errorf("expected branch 'feature/test', got %s", result.Branch)
}
if len(result.ChangedFiles) != 2 {
t.Errorf("expected 2 changed files, got %d", len(result.ChangedFiles))
}
}
func TestGitStatus_EmptyWorkDir(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// work_dir should not be in query when empty
if r.URL.Query().Get("work_dir") != "" {
t.Errorf("expected empty work_dir, got %s", r.URL.Query().Get("work_dir"))
}
resp := GitStatusResponse{IsRepo: true}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
_, err := client.GitStatus(context.Background(), "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestGitStatus_ErrorStatus(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"error":"not a git repository"}`))
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
_, err := client.GitStatus(context.Background(), "/workspace")
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "git status returned status 404") {
t.Errorf("expected 404 error, got %v", err)
}
}
func TestRunSDLC_Success(t *testing.T) {
var receivedReq SDLCRequest
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/sdlc" {
t.Errorf("expected /sdlc, got %s", r.URL.Path)
}
body, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(body, &receivedReq)
resp := SDLCResponse{
Success: true,
Output: "Feature started successfully",
Data: json.RawMessage(`{"feature_id":"feat-123"}`),
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
result, err := client.RunSDLC(context.Background(), "start", []string{"--name", "test-feature"}, "/workspace")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.Success {
t.Error("expected success=true")
}
if result.Output != "Feature started successfully" {
t.Errorf("expected output message, got %s", result.Output)
}
if result.Data == nil {
t.Error("expected data to be set")
}
// Verify request
if receivedReq.Command != "start" {
t.Errorf("expected command 'start', got %s", receivedReq.Command)
}
if len(receivedReq.Args) != 2 {
t.Errorf("expected 2 args, got %d", len(receivedReq.Args))
}
if receivedReq.WorkDir != "/workspace" {
t.Errorf("expected work dir '/workspace', got %s", receivedReq.WorkDir)
}
}
func TestRunSDLC_CommandFailed(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := SDLCResponse{
Success: false,
Output: "Command output before failure",
Error: "validation failed: missing required field",
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
result, err := client.RunSDLC(context.Background(), "validate", nil, "/workspace")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Success {
t.Error("expected success=false")
}
if result.Error != "validation failed: missing required field" {
t.Errorf("expected error message, got %s", result.Error)
}
}
func TestRunSDLC_ErrorStatus(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"error":"sdlc binary not found"}`))
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
_, err := client.RunSDLC(context.Background(), "status", nil, "/workspace")
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "sdlc returned status 500") {
t.Errorf("expected 500 error, got %v", err)
}
}
func TestRunSDLC_MalformedResponse(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{invalid`))
}))
defer server.Close()
client := NewClient(ClientConfig{BaseURL: server.URL})
_, err := client.RunSDLC(context.Background(), "status", nil, "/workspace")
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "decode response") {
t.Errorf("expected decode error, got %v", err)
}
}