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
This commit is contained in:
@@ -1,9 +1,147 @@
|
|||||||
package engine
|
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
|
// The small integers are meant to be stable, easily-typed identifiers for users to interact with
|
||||||
// important tasks.
|
// important tasks.
|
||||||
type WorkingSet struct {
|
type WorkingSet struct {
|
||||||
byUUID map[string]*Task
|
byUUID map[uuid.UUID]*Task
|
||||||
byID []string
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user