summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/api/server_test.go20
-rw-r--r--internal/executor/executor_test.go32
-rw-r--r--web/test/cancel-blocked.test.mjs68
3 files changed, 120 insertions, 0 deletions
diff --git a/internal/api/server_test.go b/internal/api/server_test.go
index 2209a69..ec927c0 100644
--- a/internal/api/server_test.go
+++ b/internal/api/server_test.go
@@ -896,6 +896,26 @@ func TestServer_CancelTask_Queued_TransitionsToCancelled(t *testing.T) {
}
}
+func TestServer_CancelTask_Blocked_TransitionsToCancelled(t *testing.T) {
+ srv, store := testServer(t)
+ createTaskWithState(t, store, "cancel-blocked-1", task.StateBlocked)
+
+ req := httptest.NewRequest("POST", "/api/tasks/cancel-blocked-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-blocked-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)
diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go
index 17982f8..f6d0179 100644
--- a/internal/executor/executor_test.go
+++ b/internal/executor/executor_test.go
@@ -964,6 +964,7 @@ func (m *minimalMockStore) UpdateTaskSummary(taskID, summary string) error
func (m *minimalMockStore) AppendTaskInteraction(taskID string, _ task.Interaction) error {
return nil
}
+func (m *minimalMockStore) UpdateTaskAgent(id string, agent task.AgentConfig) error { return nil }
func (m *minimalMockStore) lastStateUpdate() (string, task.State, bool) {
m.mu.Lock()
@@ -1159,3 +1160,34 @@ func TestPool_SpecificAgent_SkipsLoadBalancing(t *testing.T) {
t.Errorf("expected claude runner to NOT be called, got %d", claudeRunner.callCount())
}
}
+
+// TestPool_SpecificAgent_PersistsToDB verifies that if a specific agent
+// is requested, it is persisted to the database before the task runs.
+func TestPool_SpecificAgent_PersistsToDB(t *testing.T) {
+ store := testStore(t)
+ geminiRunner := &mockRunner{}
+ runners := map[string]Runner{
+ "gemini": geminiRunner,
+ }
+ logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
+ pool := NewPool(4, runners, store, logger)
+
+ tk := makeTask("persist-gemini")
+ tk.Agent.Type = "gemini"
+ store.CreateTask(tk)
+
+ if err := pool.Submit(context.Background(), tk); err != nil {
+ t.Fatalf("submit: %v", err)
+ }
+
+ <-pool.Results()
+
+ // Check the task in the database.
+ reloaded, err := store.GetTask(tk.ID)
+ if err != nil {
+ t.Fatalf("get task: %v", err)
+ }
+ if reloaded.Agent.Type != "gemini" {
+ t.Errorf("expected agent type gemini in DB, got %q", reloaded.Agent.Type)
+ }
+}
diff --git a/web/test/cancel-blocked.test.mjs b/web/test/cancel-blocked.test.mjs
new file mode 100644
index 0000000..0488345
--- /dev/null
+++ b/web/test/cancel-blocked.test.mjs
@@ -0,0 +1,68 @@
+// cancel-blocked.test.mjs — cancel button visibility for BLOCKED tasks
+//
+// Run with: node --test web/test/cancel-blocked.test.mjs
+
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
+
+// ── Logic under test ──────────────────────────────────────────────────────────
+// BLOCKED tasks must show a Cancel button (in addition to question/subtask UI).
+// The cancel button should be visible for BLOCKED, RUNNING, and other active states.
+
+const CANCEL_STATES = new Set(['RUNNING', 'BLOCKED']);
+
+function showCancelButton(state) {
+ return CANCEL_STATES.has(state);
+}
+
+function getCancelEndpoint(taskId) {
+ return `/api/tasks/${taskId}/cancel`;
+}
+
+// ── Tests ─────────────────────────────────────────────────────────────────────
+
+describe('cancel button visibility for BLOCKED tasks', () => {
+ it('shows Cancel button for BLOCKED', () => {
+ assert.equal(showCancelButton('BLOCKED'), true);
+ });
+
+ it('shows Cancel button for RUNNING', () => {
+ assert.equal(showCancelButton('RUNNING'), true);
+ });
+
+ it('does not show Cancel button for PENDING', () => {
+ assert.equal(showCancelButton('PENDING'), false);
+ });
+
+ it('does not show Cancel button for COMPLETED', () => {
+ assert.equal(showCancelButton('COMPLETED'), false);
+ });
+
+ it('does not show Cancel button for QUEUED', () => {
+ assert.equal(showCancelButton('QUEUED'), false);
+ });
+
+ it('does not show Cancel button for FAILED', () => {
+ assert.equal(showCancelButton('FAILED'), false);
+ });
+
+ it('does not show Cancel button for CANCELLED', () => {
+ assert.equal(showCancelButton('CANCELLED'), false);
+ });
+
+ it('does not show Cancel button for READY', () => {
+ assert.equal(showCancelButton('READY'), false);
+ });
+});
+
+describe('cancel API endpoint for BLOCKED tasks', () => {
+ it('uses correct cancel endpoint for a BLOCKED task', () => {
+ assert.equal(getCancelEndpoint('task-123'), '/api/tasks/task-123/cancel');
+ });
+
+ it('cancel endpoint uses POST method (no-op check — method is always POST)', () => {
+ // The cancel action always calls POST /api/tasks/{id}/cancel
+ const method = 'POST';
+ assert.equal(method, 'POST');
+ });
+});