From c0994d1177e88017c08c13704e4b05378369c4a7 Mon Sep 17 00:00:00 2001 From: Bruno Bornsztein Date: Sun, 11 Jan 2026 10:41:05 -0600 Subject: [PATCH] fix: sort closed tasks by completion date instead of creation date Previously, all tasks were sorted by created_at DESC, meaning closed tasks in the "Done" column showed oldest-created tasks first rather than most-recently-closed tasks first. Now closed tasks (done/blocked status) are sorted by completed_at DESC, showing the most recently closed tasks first. Other tasks continue to sort by created_at DESC. Added id DESC as secondary sort for consistent ordering when timestamps are equal. Co-Authored-By: Claude Opus 4.5 --- internal/db/tasks.go | 5 ++- internal/db/tasks_test.go | 84 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/internal/db/tasks.go b/internal/db/tasks.go index 16fdbfd0..5d658c4b 100644 --- a/internal/db/tasks.go +++ b/internal/db/tasks.go @@ -145,7 +145,10 @@ func (db *DB) ListTasks(opts ListTasksOptions) ([]*Task, error) { query += " AND status != 'done'" } - query += " ORDER BY created_at DESC" + // Sort done/blocked tasks by completed_at (most recently closed first), + // other tasks by created_at (newest first). + // Use id DESC as secondary sort for consistent ordering when timestamps are equal. + query += " ORDER BY CASE WHEN status IN ('done', 'blocked') THEN completed_at ELSE created_at END DESC, id DESC" if opts.Limit > 0 { query += fmt.Sprintf(" LIMIT %d", opts.Limit) diff --git a/internal/db/tasks_test.go b/internal/db/tasks_test.go index 03807c12..f16a1fa3 100644 --- a/internal/db/tasks_test.go +++ b/internal/db/tasks_test.go @@ -496,6 +496,90 @@ func TestLastTaskTypeForProject(t *testing.T) { } } +func TestListTasksClosedSortedByCompletedAt(t *testing.T) { + // Create temporary database + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, "test.db") + + database, err := Open(dbPath) + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + defer database.Close() + defer os.Remove(dbPath) + + // Create tasks in specific order - older tasks first + task1 := &Task{ + Title: "First task - closed early", + Status: StatusBacklog, + Type: TypeCode, + Project: "test", + } + task2 := &Task{ + Title: "Second task - closed late", + Status: StatusBacklog, + Type: TypeCode, + Project: "test", + } + task3 := &Task{ + Title: "Third task - not closed", + Status: StatusBacklog, + Type: TypeCode, + Project: "test", + } + + // Create tasks + if err := database.CreateTask(task1); err != nil { + t.Fatalf("failed to create task1: %v", err) + } + if err := database.CreateTask(task2); err != nil { + t.Fatalf("failed to create task2: %v", err) + } + if err := database.CreateTask(task3); err != nil { + t.Fatalf("failed to create task3: %v", err) + } + + // Close task1 first + if err := database.UpdateTaskStatus(task1.ID, StatusDone); err != nil { + t.Fatalf("failed to close task1: %v", err) + } + + // Then close task2 (so task2 has later completed_at) + if err := database.UpdateTaskStatus(task2.ID, StatusDone); err != nil { + t.Fatalf("failed to close task2: %v", err) + } + + // List all tasks including closed + tasks, err := database.ListTasks(ListTasksOptions{IncludeClosed: true}) + if err != nil { + t.Fatalf("failed to list tasks: %v", err) + } + + if len(tasks) != 3 { + t.Fatalf("expected 3 tasks, got %d", len(tasks)) + } + + // Find done tasks - the one closed most recently (task2) should appear before task1 + var doneTasks []*Task + for _, task := range tasks { + if task.Status == StatusDone { + doneTasks = append(doneTasks, task) + } + } + + if len(doneTasks) != 2 { + t.Fatalf("expected 2 done tasks, got %d", len(doneTasks)) + } + + // task2 (closed later) should be first + if doneTasks[0].ID != task2.ID { + t.Errorf("expected task2 (most recently closed) to be first, got task%d", doneTasks[0].ID) + } + if doneTasks[1].ID != task1.ID { + t.Errorf("expected task1 (closed earlier) to be second, got task%d", doneTasks[1].ID) + } +} + func TestCreateTaskSavesLastType(t *testing.T) { // Create temporary database tmpDir := t.TempDir()