package queue import ( "os" "path/filepath" "strings" "testing" "time" "github.com/jfraeys/fetch_ml/internal/queue" ) // TestTaskPrioritizationSpec documents the scheduler's priority and FIFO behavior. // These tests serve as executable specifications for the queue system. func TestTaskPrioritizationSpec(t *testing.T) { tests := []struct { name string tasks []queue.Task expected []string // IDs in expected execution order }{ { name: "higher priority runs first", tasks: []queue.Task{ {ID: "low", JobName: "low-job", Status: "queued", Priority: 1, CreatedAt: time.Unix(100, 0)}, {ID: "high", JobName: "high-job", Status: "queued", Priority: 10, CreatedAt: time.Unix(100, 0)}, }, expected: []string{"high", "low"}, }, { name: "FIFO for same priority", tasks: []queue.Task{ {ID: "first", JobName: "first-job", Status: "queued", Priority: 5, CreatedAt: time.Unix(100, 0)}, {ID: "second", JobName: "second-job", Status: "queued", Priority: 5, CreatedAt: time.Unix(200, 0)}, }, expected: []string{"first", "second"}, }, { name: "mixed priorities and creation times", tasks: []queue.Task{ {ID: "medium-early", JobName: "me-job", Status: "queued", Priority: 5, CreatedAt: time.Unix(100, 0)}, {ID: "high-late", JobName: "hl-job", Status: "queued", Priority: 10, CreatedAt: time.Unix(300, 0)}, {ID: "low-early", JobName: "le-job", Status: "queued", Priority: 1, CreatedAt: time.Unix(50, 0)}, }, expected: []string{"high-late", "medium-early", "low-early"}, }, { name: "negative priority is lowest", tasks: []queue.Task{ {ID: "negative", JobName: "neg-job", Status: "queued", Priority: -1, CreatedAt: time.Unix(100, 0)}, {ID: "positive", JobName: "pos-job", Status: "queued", Priority: 1, CreatedAt: time.Unix(100, 0)}, }, expected: []string{"positive", "negative"}, }, { name: "zero priority is default", tasks: []queue.Task{ {ID: "zero", JobName: "zero-job", Status: "queued", Priority: 0, CreatedAt: time.Unix(100, 0)}, {ID: "positive", JobName: "pos-job", Status: "queued", Priority: 1, CreatedAt: time.Unix(100, 0)}, }, expected: []string{"positive", "zero"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create a queue and add tasks tmpDir := t.TempDir() q, err := queue.NewFilesystemQueue(tmpDir) if err != nil { t.Fatalf("failed to create queue: %v", err) } defer q.Close() // Add all tasks for _, task := range tt.tasks { task := task // capture range variable if err := q.AddTask(&task); err != nil { t.Fatalf("failed to add task %s: %v", task.ID, err) } } // Get tasks in order and verify var actual []string for i := 0; i < len(tt.tasks); i++ { task, err := q.GetNextTask() if err != nil { t.Fatalf("failed to get task at position %d: %v", i, err) } if task == nil { t.Fatalf("expected task at position %d, got nil", i) } actual = append(actual, task.ID) } // Verify order if len(actual) != len(tt.expected) { t.Errorf("expected %d tasks, got %d", len(tt.expected), len(actual)) } for i, expectedID := range tt.expected { if i >= len(actual) { break } if actual[i] != expectedID { t.Errorf("position %d: expected %s, got %s", i, expectedID, actual[i]) } } }) } } // TestQueueSpec_ClaimAndComplete documents the claim-complete lifecycle func TestQueueSpec_ClaimAndComplete(t *testing.T) { tmpDir := t.TempDir() q, err := queue.NewFilesystemQueue(tmpDir) if err != nil { t.Fatalf("failed to create queue: %v", err) } defer q.Close() // Add a task task := &queue.Task{ ID: "task-1", JobName: "test-job", Status: "queued", Priority: 5, CreatedAt: time.Now(), } if err := q.AddTask(task); err != nil { t.Fatalf("failed to add task: %v", err) } // Get the task (moves it from pending to running) claimed, err := q.GetNextTask() if err != nil { t.Fatalf("failed to get task: %v", err) } if claimed == nil { t.Fatal("expected to get a task, got nil") } if claimed.ID != task.ID { t.Errorf("expected task %s, got %s", task.ID, claimed.ID) } // Verify task is no longer in pending (it's now in running) pendingDir := filepath.Join(tmpDir, "pending", "entries") entries, err := os.ReadDir(pendingDir) if err != nil { t.Fatalf("failed to read pending dir: %v", err) } for _, e := range entries { if strings.Contains(e.Name(), task.ID) { t.Error("task should not be in pending after GetNextTask") } } // Verify task is in running runningPath := filepath.Join(tmpDir, "running", task.ID+".json") if _, err := os.Stat(runningPath); os.IsNotExist(err) { t.Error("task should be in running directory after GetNextTask") } } // TestQueueSpec_TaskPriorityOrdering documents numeric priority ordering func TestQueueSpec_TaskPriorityOrdering(t *testing.T) { tmpDir := t.TempDir() q, err := queue.NewFilesystemQueue(tmpDir) if err != nil { t.Fatalf("failed to create queue: %v", err) } defer q.Close() // Add tasks with various priorities priorities := []int64{100, 50, 200, 1, 75} i := 0 for _, p := range priorities { task := &queue.Task{ ID: "task-" + string(rune('a'+i)), JobName: "job-" + string(rune('a'+i)), Status: "queued", Priority: p, CreatedAt: time.Now(), } if err := q.AddTask(task); err != nil { t.Fatalf("failed to add task: %v", err) } i++ } // Expected order: 200, 100, 75, 50, 1 (descending) expected := []string{"task-c", "task-a", "task-e", "task-b", "task-d"} for i, expID := range expected { task, err := q.GetNextTask() if err != nil { t.Fatalf("position %d: failed to get task: %v", i, err) } if task == nil { t.Fatalf("position %d: expected task %s, got nil", i, expID) } if task.ID != expID { t.Errorf("position %d: expected %s, got %s", i, expID, task.ID) } } }