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>
775 lines
22 KiB
Go
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)
|
|
}
|
|
}
|