diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-05 18:51:50 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-05 18:51:50 +0000 |
| commit | cf83444a9d341ae362e65a9f995100c69176887c (patch) | |
| tree | 0dc12aea9510d10d9e60e9c58473cbdb9db5db47 /internal/api/server_test.go | |
| parent | 680e5f668637248073c1f8f7e3547810ab1ada36 (diff) | |
Rescue work from claudomator-work: question/answer, ratelimit, start-next-task
Merges features developed in /site/doot.terst.org/claudomator-work (a
stale clone) into the canonical repo:
- executor: QuestionRegistry for human-in-the-loop answers, rate limit
detection and exponential backoff retry (ratelimit.go, question.go)
- executor/claude.go: process group isolation (SIGKILL orphans on cancel),
os.Pipe for reliable stdout drain, backoff retry on rate limits
- api/scripts.go: POST /api/scripts/start-next-task handler
- api/server.go: startNextTaskScript field, answer-question route,
BroadcastQuestion for WebSocket question events
- web: Cancel/Restart buttons, question banner UI, log viewer, validate
section, WebSocket auto-connect
All tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/api/server_test.go')
| -rw-r--r-- | internal/api/server_test.go | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 5094961..af93a77 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -335,3 +335,143 @@ func TestCORS_Headers(t *testing.T) { t.Errorf("OPTIONS status: want 200, got %d", w.Code) } } + +func TestAnswerQuestion_NoTask_Returns404(t *testing.T) { + srv, _ := testServer(t) + + payload := `{"question_id": "toolu_abc", "answer": "blue"}` + req := httptest.NewRequest("POST", "/api/tasks/nonexistent/answer", bytes.NewBufferString(payload)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("status: want 404, got %d; body: %s", w.Code, w.Body.String()) + } +} + +func TestAnswerQuestion_NoPendingQuestion_Returns404(t *testing.T) { + srv, store := testServer(t) + createTaskWithState(t, store, "answer-task-1", task.StatePending) + + payload := `{"question_id": "toolu_nonexistent", "answer": "blue"}` + req := httptest.NewRequest("POST", "/api/tasks/answer-task-1/answer", bytes.NewBufferString(payload)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("status: want 404, got %d; body: %s", w.Code, w.Body.String()) + } + var body map[string]string + json.NewDecoder(w.Body).Decode(&body) + if body["error"] != "no pending question with that ID" { + t.Errorf("error: want 'no pending question with that ID', got %q", body["error"]) + } +} + +func TestAnswerQuestion_WithPendingQuestion_Returns200(t *testing.T) { + srv, store := testServer(t) + createTaskWithState(t, store, "answer-task-2", task.StateRunning) + + ch := srv.pool.Questions.Register("answer-task-2", "toolu_Q1", []byte(`{}`)) + + go func() { + payload := `{"question_id": "toolu_Q1", "answer": "red"}` + req := httptest.NewRequest("POST", "/api/tasks/answer-task-2/answer", bytes.NewBufferString(payload)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("status: want 200, got %d; body: %s", w.Code, w.Body.String()) + } + }() + + answer := <-ch + if answer != "red" { + t.Errorf("answer: want 'red', got %q", answer) + } +} + +func TestAnswerQuestion_MissingQuestionID_Returns400(t *testing.T) { + srv, store := testServer(t) + createTaskWithState(t, store, "answer-task-3", task.StateRunning) + + payload := `{"answer": "blue"}` + req := httptest.NewRequest("POST", "/api/tasks/answer-task-3/answer", bytes.NewBufferString(payload)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("status: want 400, got %d", w.Code) + } +} + +func TestHandleStartNextTask_Success(t *testing.T) { + dir := t.TempDir() + script := filepath.Join(dir, "start-next-task") + if err := os.WriteFile(script, []byte("#!/bin/sh\necho 'claudomator start abc-123'\n"), 0755); err != nil { + t.Fatal(err) + } + + srv, _ := testServer(t) + srv.startNextTaskScript = script + + req := httptest.NewRequest("POST", "/api/scripts/start-next-task", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("want 200, got %d; body: %s", w.Code, w.Body.String()) + } + var body map[string]interface{} + json.NewDecoder(w.Body).Decode(&body) + if body["output"] != "claudomator start abc-123\n" { + t.Errorf("unexpected output: %v", body["output"]) + } + if body["exit_code"] != float64(0) { + t.Errorf("unexpected exit_code: %v", body["exit_code"]) + } +} + +func TestHandleStartNextTask_NoTask(t *testing.T) { + dir := t.TempDir() + script := filepath.Join(dir, "start-next-task") + if err := os.WriteFile(script, []byte("#!/bin/sh\necho 'No task to start.'\n"), 0755); err != nil { + t.Fatal(err) + } + + srv, _ := testServer(t) + srv.startNextTaskScript = script + + req := httptest.NewRequest("POST", "/api/scripts/start-next-task", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("want 200, got %d; body: %s", w.Code, w.Body.String()) + } + var body map[string]interface{} + json.NewDecoder(w.Body).Decode(&body) + if body["output"] != "No task to start.\n" { + t.Errorf("unexpected output: %v", body["output"]) + } +} + +func TestHandleStartNextTask_ScriptNotFound(t *testing.T) { + srv, _ := testServer(t) + srv.startNextTaskScript = "/nonexistent/start-next-task" + + req := httptest.NewRequest("POST", "/api/scripts/start-next-task", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("want 500, got %d; body: %s", w.Code, w.Body.String()) + } +} |
