diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-02-08 21:35:45 -1000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-02-08 21:35:45 -1000 |
| commit | 2e2b2187b957e9af78797a67ec5c6874615fae02 (patch) | |
| tree | 1181dbb7e43f5d30cb025fa4d50fd4e7a2c893b3 /internal/api/server_test.go | |
Initial project: task model, executor, API server, CLI, storage, reporter
Claudomator automation toolkit for Claude Code with:
- Task model with YAML parsing, validation, state machine (49 tests, 0 races)
- SQLite storage for tasks and executions
- Executor pool with bounded concurrency, timeout, cancellation
- REST API + WebSocket for mobile PWA integration
- Webhook/multi-notifier system
- CLI: init, run, serve, list, status commands
- Console, JSON, HTML reporters with cost tracking
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/api/server_test.go')
| -rw-r--r-- | internal/api/server_test.go | 186 |
1 files changed, 186 insertions, 0 deletions
diff --git a/internal/api/server_test.go b/internal/api/server_test.go new file mode 100644 index 0000000..c3b77ae --- /dev/null +++ b/internal/api/server_test.go @@ -0,0 +1,186 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "context" + + "github.com/claudomator/claudomator/internal/executor" + "github.com/claudomator/claudomator/internal/storage" + "github.com/claudomator/claudomator/internal/task" +) + +func testServer(t *testing.T) (*Server, *storage.DB) { + t.Helper() + dbPath := filepath.Join(t.TempDir(), "test.db") + store, err := storage.Open(dbPath) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { store.Close() }) + + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError})) + runner := &mockRunner{} + pool := executor.NewPool(2, runner, store, logger) + srv := NewServer(store, pool, logger) + return srv, store +} + +type mockRunner struct{} + +func (m *mockRunner) Run(_ context.Context, _ *task.Task, _ *storage.Execution) error { + return nil +} + +func TestHealthEndpoint(t *testing.T) { + srv, _ := testServer(t) + req := httptest.NewRequest("GET", "/api/health", nil) + w := httptest.NewRecorder() + + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("status: want 200, got %d", w.Code) + } + var body map[string]string + json.NewDecoder(w.Body).Decode(&body) + if body["status"] != "ok" { + t.Errorf("want status=ok, got %v", body) + } +} + +func TestCreateTask_Success(t *testing.T) { + srv, _ := testServer(t) + + payload := `{ + "name": "API Task", + "description": "Created via API", + "claude": { + "instructions": "do the thing", + "model": "sonnet" + }, + "timeout": "5m", + "tags": ["api"] + }` + req := httptest.NewRequest("POST", "/api/tasks", bytes.NewBufferString(payload)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusCreated { + t.Fatalf("status: want 201, got %d; body: %s", w.Code, w.Body.String()) + } + + var created task.Task + json.NewDecoder(w.Body).Decode(&created) + if created.Name != "API Task" { + t.Errorf("name: want 'API Task', got %q", created.Name) + } + if created.ID == "" { + t.Error("expected auto-generated ID") + } +} + +func TestCreateTask_InvalidJSON(t *testing.T) { + srv, _ := testServer(t) + + req := httptest.NewRequest("POST", "/api/tasks", bytes.NewBufferString("{bad json")) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("status: want 400, got %d", w.Code) + } +} + +func TestCreateTask_ValidationFailure(t *testing.T) { + srv, _ := testServer(t) + + payload := `{"name": "", "claude": {"instructions": ""}}` + req := httptest.NewRequest("POST", "/api/tasks", bytes.NewBufferString(payload)) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("status: want 400, got %d", w.Code) + } +} + +func TestListTasks_Empty(t *testing.T) { + srv, _ := testServer(t) + + req := httptest.NewRequest("GET", "/api/tasks", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("status: want 200, got %d", w.Code) + } + + var tasks []task.Task + json.NewDecoder(w.Body).Decode(&tasks) + if len(tasks) != 0 { + t.Errorf("want 0 tasks, got %d", len(tasks)) + } +} + +func TestGetTask_NotFound(t *testing.T) { + srv, _ := testServer(t) + + req := httptest.NewRequest("GET", "/api/tasks/nonexistent", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("status: want 404, got %d", w.Code) + } +} + +func TestListTasks_WithTasks(t *testing.T) { + srv, store := testServer(t) + + // Create tasks directly in store. + for i := 0; i < 3; i++ { + tk := &task.Task{ + ID: fmt.Sprintf("lt-%d", i), Name: fmt.Sprintf("T%d", i), + Claude: task.ClaudeConfig{Instructions: "x"}, Priority: task.PriorityNormal, + Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"}, + Tags: []string{}, DependsOn: []string{}, State: task.StatePending, + } + store.CreateTask(tk) + } + + req := httptest.NewRequest("GET", "/api/tasks", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + var tasks []task.Task + json.NewDecoder(w.Body).Decode(&tasks) + if len(tasks) != 3 { + t.Errorf("want 3 tasks, got %d", len(tasks)) + } +} + +func TestCORS_Headers(t *testing.T) { + srv, _ := testServer(t) + + req := httptest.NewRequest("OPTIONS", "/api/tasks", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Header().Get("Access-Control-Allow-Origin") != "*" { + t.Error("missing CORS origin header") + } + if w.Code != http.StatusOK { + t.Errorf("OPTIONS status: want 200, got %d", w.Code) + } +} |
