summaryrefslogtreecommitdiff
path: root/internal/api/server_test.go
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator>2026-03-12 01:48:15 +0000
committerClaudomator Agent <agent@claudomator>2026-03-12 01:48:15 +0000
commitf28c22352aa1a8ede7552ee0277f7d60552d9094 (patch)
tree02e9085809462e09b7d2a68ebb754e247eaecd22 /internal/api/server_test.go
parent4f83d35fa47bc71b31e0f92a0927bea8910c01b6 (diff)
feat: add Resume support for CANCELLED, FAILED, and BUDGET_EXCEEDED tasks
Interrupted tasks (CANCELLED, FAILED, BUDGET_EXCEEDED) now support session resume in addition to restart. Both buttons are shown on the task card. - executor: extend resumablePoolStates to include CANCELLED, FAILED, BUDGET_EXCEEDED - api: extend handleResumeTimedOutTask to accept all resumable states with state-specific resume messages; replace hard-coded TIMED_OUT check with a resumableStates map - web: add RESUME_STATES set; render Resume + Restart buttons for interrupted states; TIMED_OUT keeps Resume only - tests: 5 new Go tests (TestResumeInterrupted_*); updated task-actions.test.mjs with 17 tests covering dual-button behaviour 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.go123
1 files changed, 119 insertions, 4 deletions
diff --git a/internal/api/server_test.go b/internal/api/server_test.go
index afdc9d2..c90e3b3 100644
--- a/internal/api/server_test.go
+++ b/internal/api/server_test.go
@@ -894,10 +894,11 @@ func TestServer_CancelTask_Completed_Returns409(t *testing.T) {
// mockQuestionStore implements questionStore for testing handleAnswerQuestion.
type mockQuestionStore struct {
- getTaskFn func(id string) (*task.Task, error)
- getLatestExecutionFn func(taskID string) (*storage.Execution, error)
- updateTaskQuestionFn func(taskID, questionJSON string) error
- updateTaskStateFn func(id string, newState task.State) error
+ getTaskFn func(id string) (*task.Task, error)
+ getLatestExecutionFn func(taskID string) (*storage.Execution, error)
+ updateTaskQuestionFn func(taskID, questionJSON string) error
+ updateTaskStateFn func(id string, newState task.State) error
+ appendInteractionFn func(taskID string, interaction task.Interaction) error
}
func (m *mockQuestionStore) GetTask(id string) (*task.Task, error) {
@@ -912,6 +913,12 @@ func (m *mockQuestionStore) UpdateTaskQuestion(taskID, questionJSON string) erro
func (m *mockQuestionStore) UpdateTaskState(id string, newState task.State) error {
return m.updateTaskStateFn(id, newState)
}
+func (m *mockQuestionStore) AppendTaskInteraction(taskID string, interaction task.Interaction) error {
+ if m.appendInteractionFn != nil {
+ return m.appendInteractionFn(taskID, interaction)
+ }
+ return nil
+}
func TestServer_AnswerQuestion_UpdateQuestionFails_Returns500(t *testing.T) {
srv, _ := testServer(t)
@@ -1178,6 +1185,114 @@ func TestResumeTimedOut_ResponseShape(t *testing.T) {
}
}
+func TestResumeInterrupted_Cancelled_Success_Returns202(t *testing.T) {
+ srv, store := testServer(t)
+ createTaskWithState(t, store, "resume-cancelled-1", task.StateCancelled)
+
+ exec := &storage.Execution{
+ ID: "exec-cancelled-1",
+ TaskID: "resume-cancelled-1",
+ SessionID: "550e8400-e29b-41d4-a716-446655440030",
+ Status: "CANCELLED",
+ }
+ if err := store.CreateExecution(exec); err != nil {
+ t.Fatalf("create execution: %v", err)
+ }
+
+ req := httptest.NewRequest("POST", "/api/tasks/resume-cancelled-1/resume", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusAccepted {
+ t.Errorf("status: want 202, got %d; body: %s", w.Code, w.Body.String())
+ }
+ got, _ := store.GetTask("resume-cancelled-1")
+ if got.State != task.StateQueued && got.State != task.StateRunning && got.State != task.StateReady {
+ t.Errorf("task state: want QUEUED/RUNNING/READY after resume, got %v", got.State)
+ }
+}
+
+func TestResumeInterrupted_Failed_Success_Returns202(t *testing.T) {
+ srv, store := testServer(t)
+ createTaskWithState(t, store, "resume-failed-1", task.StateFailed)
+
+ exec := &storage.Execution{
+ ID: "exec-failed-1",
+ TaskID: "resume-failed-1",
+ SessionID: "550e8400-e29b-41d4-a716-446655440031",
+ Status: "FAILED",
+ }
+ if err := store.CreateExecution(exec); err != nil {
+ t.Fatalf("create execution: %v", err)
+ }
+
+ req := httptest.NewRequest("POST", "/api/tasks/resume-failed-1/resume", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusAccepted {
+ t.Errorf("status: want 202, got %d; body: %s", w.Code, w.Body.String())
+ }
+ got, _ := store.GetTask("resume-failed-1")
+ if got.State != task.StateQueued && got.State != task.StateRunning && got.State != task.StateReady {
+ t.Errorf("task state: want QUEUED/RUNNING/READY after resume, got %v", got.State)
+ }
+}
+
+func TestResumeInterrupted_BudgetExceeded_Success_Returns202(t *testing.T) {
+ srv, store := testServer(t)
+ createTaskWithState(t, store, "resume-budget-1", task.StateBudgetExceeded)
+
+ exec := &storage.Execution{
+ ID: "exec-budget-1",
+ TaskID: "resume-budget-1",
+ SessionID: "550e8400-e29b-41d4-a716-446655440032",
+ Status: "BUDGET_EXCEEDED",
+ }
+ if err := store.CreateExecution(exec); err != nil {
+ t.Fatalf("create execution: %v", err)
+ }
+
+ req := httptest.NewRequest("POST", "/api/tasks/resume-budget-1/resume", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusAccepted {
+ t.Errorf("status: want 202, got %d; body: %s", w.Code, w.Body.String())
+ }
+ got, _ := store.GetTask("resume-budget-1")
+ if got.State != task.StateQueued && got.State != task.StateRunning && got.State != task.StateReady {
+ t.Errorf("task state: want QUEUED/RUNNING/READY after resume, got %v", got.State)
+ }
+}
+
+func TestResumeInterrupted_NoSession_Returns500(t *testing.T) {
+ srv, store := testServer(t)
+ createTaskWithState(t, store, "resume-nosess-1", task.StateCancelled)
+
+ // No execution — no session ID available.
+ req := httptest.NewRequest("POST", "/api/tasks/resume-nosess-1/resume", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusInternalServerError {
+ t.Errorf("status: want 500, got %d; body: %s", w.Code, w.Body.String())
+ }
+}
+
+func TestResumeInterrupted_WrongState_Returns409(t *testing.T) {
+ srv, store := testServer(t)
+ createTaskWithState(t, store, "resume-wrong-1", task.StatePending)
+
+ req := httptest.NewRequest("POST", "/api/tasks/resume-wrong-1/resume", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusConflict {
+ t.Errorf("status: want 409, got %d; body: %s", w.Code, w.Body.String())
+ }
+}
+
func TestRateLimit_ValidateRejectsExcess(t *testing.T) {
srv, _ := testServer(t)
srv.elaborateLimiter = newIPRateLimiter(0, 1)