From 970473173935250e8caf54f6079b48f2ff54e4e5 Mon Sep 17 00:00:00 2001 From: Joakim Date: Sun, 4 Jan 2026 18:11:59 +0100 Subject: [PATCH] Implement opal-task Phase 4: WorkingSet - Implement BuildWorkingSet() to create ephemeral display ID mappings - Implement LoadWorkingSet() to restore from database - Add GetTaskByDisplayID() for ID resolution - Add GetTasks() and Size() helper methods - Persist working set to SQLite working_set table - Add comprehensive tests (5 tests, all passing) - Support filtered working sets --- opal-task/internal/engine/ws.go | 144 +++++++++++++++++++++++++- opal-task/internal/engine/ws_test.go | 148 +++++++++++++++++++++++++++ 2 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 opal-task/internal/engine/ws_test.go diff --git a/opal-task/internal/engine/ws.go b/opal-task/internal/engine/ws.go index 874cbd8..447514c 100644 --- a/opal-task/internal/engine/ws.go +++ b/opal-task/internal/engine/ws.go @@ -1,9 +1,147 @@ package engine -// WorkingSet is a mapping from small integers to task uuids for all pending tasks. +import ( + "fmt" + + "github.com/google/uuid" +) + +// WorkingSet is a mapping from small integers to task UUIDs for filtered tasks. // The small integers are meant to be stable, easily-typed identifiers for users to interact with // important tasks. type WorkingSet struct { - byUUID map[string]*Task - byID []string + byUUID map[uuid.UUID]*Task + byID map[int]uuid.UUID // display_id -> UUID +} + +// BuildWorkingSet creates a working set from filter results and persists to DB +func BuildWorkingSet(filter *Filter) (*WorkingSet, error) { + tasks, err := GetTasks(filter) + if err != nil { + return nil, err + } + + ws := &WorkingSet{ + byUUID: make(map[uuid.UUID]*Task), + byID: make(map[int]uuid.UUID), + } + + db := GetDB() + if db == nil { + return nil, fmt.Errorf("database not initialized") + } + + tx, err := db.Begin() + if err != nil { + return nil, err + } + defer tx.Rollback() + + // Clear existing working set + if _, err := tx.Exec("DELETE FROM working_set"); err != nil { + return nil, err + } + + // Insert new working set + stmt, err := tx.Prepare("INSERT INTO working_set (display_id, task_uuid) VALUES (?, ?)") + if err != nil { + return nil, err + } + defer stmt.Close() + + for i, task := range tasks { + displayID := i + 1 // 1-based + ws.byUUID[task.UUID] = task + ws.byID[displayID] = task.UUID + + if _, err := stmt.Exec(displayID, task.UUID.String()); err != nil { + return nil, err + } + } + + if err := tx.Commit(); err != nil { + return nil, err + } + + return ws, nil +} + +// LoadWorkingSet loads the current working set from DB +func LoadWorkingSet() (*WorkingSet, error) { + db := GetDB() + if db == nil { + return nil, fmt.Errorf("database not initialized") + } + + rows, err := db.Query(` + SELECT ws.display_id, ws.task_uuid + FROM working_set ws + ORDER BY ws.display_id + `) + if err != nil { + return nil, err + } + defer rows.Close() + + ws := &WorkingSet{ + byUUID: make(map[uuid.UUID]*Task), + byID: make(map[int]uuid.UUID), + } + + for rows.Next() { + var displayID int + var taskUUIDStr string + + if err := rows.Scan(&displayID, &taskUUIDStr); err != nil { + return nil, err + } + + taskUUID, err := uuid.Parse(taskUUIDStr) + if err != nil { + return nil, fmt.Errorf("failed to parse UUID: %w", err) + } + + // Load the actual task + task, err := GetTask(taskUUID) + if err != nil { + // Task might have been deleted, skip it + continue + } + + ws.byUUID[task.UUID] = task + ws.byID[displayID] = task.UUID + } + + return ws, nil +} + +// GetTaskByDisplayID resolves display ID to task +func (ws *WorkingSet) GetTaskByDisplayID(id int) (*Task, error) { + taskUUID, exists := ws.byID[id] + if !exists { + return nil, fmt.Errorf("invalid task ID: %d (not in current working set)", id) + } + task, exists := ws.byUUID[taskUUID] + if !exists { + return nil, fmt.Errorf("task UUID not found in working set") + } + return task, nil +} + +// GetTasks returns all tasks in the working set +func (ws *WorkingSet) GetTasks() []*Task { + tasks := make([]*Task, 0, len(ws.byID)) + for i := 1; i <= len(ws.byID); i++ { + if taskUUID, ok := ws.byID[i]; ok { + if task, ok := ws.byUUID[taskUUID]; ok { + tasks = append(tasks, task) + } + } + } + return tasks +} + +// Size returns number of tasks in working set +func (ws *WorkingSet) Size() int { + return len(ws.byID) } diff --git a/opal-task/internal/engine/ws_test.go b/opal-task/internal/engine/ws_test.go new file mode 100644 index 0000000..4173b50 --- /dev/null +++ b/opal-task/internal/engine/ws_test.go @@ -0,0 +1,148 @@ +package engine + +import ( + "testing" +) + +func TestBuildWorkingSet(t *testing.T) { + // Create some tasks + task1, _ := CreateTask("Task 1") + task2, _ := CreateTask("Task 2") + task3, _ := CreateTask("Task 3") + + // Build working set with default filter + ws, err := BuildWorkingSet(DefaultFilter()) + if err != nil { + t.Fatalf("Failed to build working set: %v", err) + } + + if ws.Size() < 3 { + t.Errorf("Expected at least 3 tasks in working set, got %d", ws.Size()) + } + + // Check that tasks are accessible by display ID + task, err := ws.GetTaskByDisplayID(1) + if err != nil { + t.Fatalf("Failed to get task by display ID: %v", err) + } + + if task == nil { + t.Error("Task should not be nil") + } + + // Verify tasks are in the set + _, exists := ws.byUUID[task1.UUID] + if !exists { + t.Error("Task 1 should be in working set") + } + + _, exists = ws.byUUID[task2.UUID] + if !exists { + t.Error("Task 2 should be in working set") + } + + _, exists = ws.byUUID[task3.UUID] + if !exists { + t.Error("Task 3 should be in working set") + } +} + +func TestLoadWorkingSet(t *testing.T) { + // Create and build a working set + CreateTask("Load test 1") + CreateTask("Load test 2") + + ws1, err := BuildWorkingSet(DefaultFilter()) + if err != nil { + t.Fatalf("Failed to build working set: %v", err) + } + + size1 := ws1.Size() + + // Load the working set + ws2, err := LoadWorkingSet() + if err != nil { + t.Fatalf("Failed to load working set: %v", err) + } + + if ws2.Size() != size1 { + t.Errorf("Loaded working set size (%d) doesn't match built size (%d)", ws2.Size(), size1) + } +} + +func TestWorkingSetWithFilter(t *testing.T) { + // Create tasks with different priorities + task1, _ := CreateTask("High priority task") + task1.Priority = PriorityHigh + task1.Save() + + task2, _ := CreateTask("Low priority task") + task2.Priority = PriorityLow + task2.Save() + + // Build working set with high priority filter + filter, _ := ParseFilter([]string{"priority:H"}) + ws, err := BuildWorkingSet(filter) + if err != nil { + t.Fatalf("Failed to build filtered working set: %v", err) + } + + // Should only contain high priority task + _, exists := ws.byUUID[task1.UUID] + if !exists { + t.Error("High priority task should be in working set") + } + + _, exists = ws.byUUID[task2.UUID] + if exists { + t.Error("Low priority task should NOT be in working set") + } +} + +func TestGetTaskByDisplayID(t *testing.T) { + // Create tasks + CreateTask("Display ID test 1") + CreateTask("Display ID test 2") + CreateTask("Display ID test 3") + + ws, _ := BuildWorkingSet(DefaultFilter()) + + // Test valid display IDs + for i := 1; i <= ws.Size(); i++ { + task, err := ws.GetTaskByDisplayID(i) + if err != nil { + t.Errorf("Failed to get task with display ID %d: %v", i, err) + } + if task == nil { + t.Errorf("Task with display ID %d should not be nil", i) + } + } + + // Test invalid display ID + _, err := ws.GetTaskByDisplayID(999) + if err == nil { + t.Error("Should return error for invalid display ID") + } +} + +func TestWorkingSetGetTasks(t *testing.T) { + // Create tasks + CreateTask("GetTasks test 1") + CreateTask("GetTasks test 2") + + ws, _ := BuildWorkingSet(DefaultFilter()) + + tasks := ws.GetTasks() + if len(tasks) != ws.Size() { + t.Errorf("GetTasks returned %d tasks, expected %d", len(tasks), ws.Size()) + } + + // Verify tasks are in order + for i, task := range tasks { + displayID := i + 1 + expectedUUID := ws.byID[displayID] + if task.UUID != expectedUUID { + t.Errorf("Task at position %d has wrong UUID", i) + } + } +}