summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/server.go39
-rw-r--r--internal/api/server_test.go70
2 files changed, 65 insertions, 44 deletions
diff --git a/internal/api/server.go b/internal/api/server.go
index bac98b6..5758347 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -112,31 +112,52 @@ func (s *Server) BroadcastQuestion(taskID, toolUseID string, questionData json.R
func (s *Server) handleAnswerQuestion(w http.ResponseWriter, r *http.Request) {
taskID := r.PathValue("id")
- if _, err := s.store.GetTask(taskID); err != nil {
+ tk, err := s.store.GetTask(taskID)
+ if err != nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "task not found"})
return
}
+ if tk.State != task.StateBlocked {
+ writeJSON(w, http.StatusConflict, map[string]string{"error": "task is not blocked"})
+ return
+ }
var input struct {
- QuestionID string `json:"question_id"`
- Answer string `json:"answer"`
+ Answer string `json:"answer"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()})
return
}
- if input.QuestionID == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "question_id is required"})
+ if input.Answer == "" {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "answer is required"})
+ return
+ }
+
+ // Look up the session ID from the most recent execution.
+ latest, err := s.store.GetLatestExecution(taskID)
+ if err != nil || latest.SessionID == "" {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "no resumable session found"})
return
}
- ok := s.pool.Questions.Answer(input.QuestionID, input.Answer)
- if !ok {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "no pending question with that ID"})
+ // Clear the question and transition to QUEUED.
+ s.store.UpdateTaskQuestion(taskID, "")
+ s.store.UpdateTaskState(taskID, task.StateQueued)
+
+ // Submit a resume execution.
+ resumeExec := &storage.Execution{
+ ID: uuid.New().String(),
+ TaskID: taskID,
+ ResumeSessionID: latest.SessionID,
+ ResumeAnswer: input.Answer,
+ }
+ if err := s.pool.SubmitResume(r.Context(), tk, resumeExec); err != nil {
+ writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": err.Error()})
return
}
- writeJSON(w, http.StatusOK, map[string]string{"status": "delivered"})
+ writeJSON(w, http.StatusOK, map[string]string{"status": "queued"})
}
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
diff --git a/internal/api/server_test.go b/internal/api/server_test.go
index af93a77..2325b0b 100644
--- a/internal/api/server_test.go
+++ b/internal/api/server_test.go
@@ -339,8 +339,7 @@ func TestCORS_Headers(t *testing.T) {
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 := httptest.NewRequest("POST", "/api/tasks/nonexistent/answer", bytes.NewBufferString(`{"answer":"blue"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
@@ -351,64 +350,65 @@ func TestAnswerQuestion_NoTask_Returns404(t *testing.T) {
}
}
-func TestAnswerQuestion_NoPendingQuestion_Returns404(t *testing.T) {
+func TestAnswerQuestion_TaskNotBlocked_Returns409(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 := httptest.NewRequest("POST", "/api/tasks/answer-task-1/answer", bytes.NewBufferString(`{"answer":"blue"}`))
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"])
+ if w.Code != http.StatusConflict {
+ t.Errorf("status: want 409, got %d; body: %s", w.Code, w.Body.String())
}
}
-func TestAnswerQuestion_WithPendingQuestion_Returns200(t *testing.T) {
+func TestAnswerQuestion_MissingAnswer_Returns400(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(`{}`))
+ createTaskWithState(t, store, "answer-task-2", task.StateBlocked)
- 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)
+ req := httptest.NewRequest("POST", "/api/tasks/answer-task-2/answer", bytes.NewBufferString(`{}`))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
- if w.Code != http.StatusOK {
- t.Errorf("status: want 200, got %d; body: %s", w.Code, w.Body.String())
- }
- }()
+ srv.Handler().ServeHTTP(w, req)
- answer := <-ch
- if answer != "red" {
- t.Errorf("answer: want 'red', got %q", answer)
+ if w.Code != http.StatusBadRequest {
+ t.Errorf("status: want 400, got %d; body: %s", w.Code, w.Body.String())
}
}
-func TestAnswerQuestion_MissingQuestionID_Returns400(t *testing.T) {
+func TestAnswerQuestion_BlockedTask_QueuesResume(t *testing.T) {
srv, store := testServer(t)
- createTaskWithState(t, store, "answer-task-3", task.StateRunning)
+ createTaskWithState(t, store, "answer-task-3", task.StateBlocked)
+
+ // Create an execution with a session ID, as the runner would have.
+ exec := &storage.Execution{
+ ID: "exec-blocked-1",
+ TaskID: "answer-task-3",
+ SessionID: "550e8400-e29b-41d4-a716-446655440001",
+ Status: "BLOCKED",
+ }
+ if err := store.CreateExecution(exec); err != nil {
+ t.Fatalf("create execution: %v", err)
+ }
- payload := `{"answer": "blue"}`
- req := httptest.NewRequest("POST", "/api/tasks/answer-task-3/answer", bytes.NewBufferString(payload))
+ req := httptest.NewRequest("POST", "/api/tasks/answer-task-3/answer", bytes.NewBufferString(`{"answer":"main"}`))
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)
+ if w.Code != http.StatusOK {
+ t.Errorf("status: want 200, got %d; body: %s", w.Code, w.Body.String())
+ }
+
+ // Task should now be QUEUED (or RUNNING since the mock runner is instant).
+ got, _ := store.GetTask("answer-task-3")
+ if got.State != task.StateQueued && got.State != task.StateRunning && got.State != task.StateReady {
+ t.Errorf("task state: want QUEUED/RUNNING/READY after answer, got %v", got.State)
}
}