From fd42a54d96fcd3342941caaeb61a4b0d5d3f1b4f Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Fri, 6 Mar 2026 23:55:07 +0000 Subject: recover: restore untracked work from recovery branch (no Gemini changes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recovered files with no Claude→Agent contamination: - docs/adr/002-task-state-machine.md - internal/api/logs.go/logs_test.go: task-level log streaming endpoint - internal/api/validate.go/validate_test.go: POST /api/tasks/validate - internal/api/server_test.go, storage/db_test.go: expanded test coverage - scripts/reset-failed-tasks, reset-running-tasks - web/app.js, index.html, style.css: frontend improvements - web/test/: active-tasks-tab, delete-button, filter-tabs, sort-tasks tests Manually applied from server.go diff (skipping Claude→Agent rename): - taskLogStore field + validateCmdPath field - DELETE /api/tasks/{id} route + handleDeleteTask - GET /api/tasks/{id}/logs/stream route - POST /api/tasks/{id}/resume route + handleResumeTimedOutTask - handleCancelTask: allow cancelling PENDING/QUEUED tasks directly Co-Authored-By: Claude Sonnet 4.6 --- internal/api/server_test.go | 182 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) (limited to 'internal/api/server_test.go') diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 2325b0b..e012bc1 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -463,6 +463,73 @@ func TestHandleStartNextTask_NoTask(t *testing.T) { } } +func TestResumeTimedOut_NoTask_Returns404(t *testing.T) { + srv, _ := testServer(t) + + req := httptest.NewRequest("POST", "/api/tasks/nonexistent/resume", nil) + 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 TestResumeTimedOut_TaskNotTimedOut_Returns409(t *testing.T) { + srv, store := testServer(t) + createTaskWithState(t, store, "resume-task-1", task.StatePending) + + req := httptest.NewRequest("POST", "/api/tasks/resume-task-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 TestResumeTimedOut_NoSession_Returns500(t *testing.T) { + srv, store := testServer(t) + createTaskWithState(t, store, "resume-task-2", task.StateTimedOut) + + // No execution created — so no session ID. + req := httptest.NewRequest("POST", "/api/tasks/resume-task-2/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 TestResumeTimedOut_Success_Returns202(t *testing.T) { + srv, store := testServer(t) + createTaskWithState(t, store, "resume-task-3", task.StateTimedOut) + + exec := &storage.Execution{ + ID: "exec-timedout-1", + TaskID: "resume-task-3", + SessionID: "550e8400-e29b-41d4-a716-446655440002", + Status: "TIMED_OUT", + } + if err := store.CreateExecution(exec); err != nil { + t.Fatalf("create execution: %v", err) + } + + req := httptest.NewRequest("POST", "/api/tasks/resume-task-3/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-task-3") + 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 TestHandleStartNextTask_ScriptNotFound(t *testing.T) { srv, _ := testServer(t) srv.startNextTaskScript = "/nonexistent/start-next-task" @@ -475,3 +542,118 @@ func TestHandleStartNextTask_ScriptNotFound(t *testing.T) { t.Errorf("want 500, got %d; body: %s", w.Code, w.Body.String()) } } + +func TestDeleteTask_Success(t *testing.T) { + srv, store := testServer(t) + + // Create a task to delete. + created := createTestTask(t, srv, `{"name":"Delete Me","claude":{"instructions":"x","model":"sonnet"}}`) + + req := httptest.NewRequest("DELETE", "/api/tasks/"+created.ID, nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusNoContent { + t.Fatalf("want 204, got %d; body: %s", w.Code, w.Body.String()) + } + + _, err := store.GetTask(created.ID) + if err == nil { + t.Error("task should be deleted from store") + } +} + +func TestDeleteTask_NotFound(t *testing.T) { + srv, _ := testServer(t) + + req := httptest.NewRequest("DELETE", "/api/tasks/does-not-exist", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("want 404, got %d", w.Code) + } +} + +func TestDeleteTask_RunningTaskRejected(t *testing.T) { + srv, store := testServer(t) + + created := createTestTask(t, srv, `{"name":"Running Task","claude":{"instructions":"x","model":"sonnet"}}`) + store.UpdateTaskState(created.ID, "RUNNING") + + req := httptest.NewRequest("DELETE", "/api/tasks/"+created.ID, nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusConflict { + t.Errorf("want 409 for running task, got %d", w.Code) + } +} + +// createTestTask is a helper that POSTs a task and returns the parsed Task. +func createTestTask(t *testing.T, srv *Server, payload string) task.Task { + t.Helper() + 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("createTestTask: want 201, got %d; body: %s", w.Code, w.Body.String()) + } + var tk task.Task + json.NewDecoder(w.Body).Decode(&tk) + return tk +} + +func TestServer_CancelTask_Pending_TransitionsToCancelled(t *testing.T) { + srv, store := testServer(t) + createTaskWithState(t, store, "cancel-pending-1", task.StatePending) + + req := httptest.NewRequest("POST", "/api/tasks/cancel-pending-1/cancel", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status: want 200, got %d; body: %s", w.Code, w.Body.String()) + } + updated, err := store.GetTask("cancel-pending-1") + if err != nil { + t.Fatal(err) + } + if updated.State != task.StateCancelled { + t.Errorf("state: want CANCELLED, got %s", updated.State) + } +} + +func TestServer_CancelTask_Queued_TransitionsToCancelled(t *testing.T) { + srv, store := testServer(t) + createTaskWithState(t, store, "cancel-queued-1", task.StateQueued) + + req := httptest.NewRequest("POST", "/api/tasks/cancel-queued-1/cancel", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status: want 200, got %d; body: %s", w.Code, w.Body.String()) + } + updated, err := store.GetTask("cancel-queued-1") + if err != nil { + t.Fatal(err) + } + if updated.State != task.StateCancelled { + t.Errorf("state: want CANCELLED, got %s", updated.State) + } +} + +func TestServer_CancelTask_Completed_Returns409(t *testing.T) { + srv, store := testServer(t) + createTaskWithState(t, store, "cancel-completed-1", task.StateCompleted) + + req := httptest.NewRequest("POST", "/api/tasks/cancel-completed-1/cancel", 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()) + } +} -- cgit v1.2.3