Implement opal-task Phase 3: Filter and Modifier Parsing
- Add filter.go: Parse filters (+tag, -tag, attribute:value, IDs) - Implement Filter.ToSQL() for WHERE clause generation - Add modifier.go: Parse modifiers (set/clear attributes, add/remove tags) - Implement Modifier.Apply() to update existing tasks - Add dateparse.go: Smart date parsing (ISO, today, tomorrow, weekdays) - Implement nextWeekday logic (smart Sunday interpretation) - Update GetTasks() to accept Filter parameter - Add CreateTaskWithModifier() for task creation with modifiers - Add comprehensive test suite (13 new tests, all passing) - Support filtering by status, project, priority, tags, UUIDs, display IDs - Support modifying priority, project, dates, recurrence, tags
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseModifier(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
checkFn func(*testing.T, *Modifier)
|
||||
}{
|
||||
{
|
||||
name: "add tags",
|
||||
args: []string{"+urgent", "+work"},
|
||||
checkFn: func(t *testing.T, m *Modifier) {
|
||||
if len(m.AddTags) != 2 {
|
||||
t.Errorf("Expected 2 add tags, got %d", len(m.AddTags))
|
||||
}
|
||||
if m.AddTags[0] != "urgent" || m.AddTags[1] != "work" {
|
||||
t.Error("Tags not parsed correctly")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove tags",
|
||||
args: []string{"-someday", "-later"},
|
||||
checkFn: func(t *testing.T, m *Modifier) {
|
||||
if len(m.RemoveTags) != 2 {
|
||||
t.Errorf("Expected 2 remove tags, got %d", len(m.RemoveTags))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set attributes",
|
||||
args: []string{"priority:H", "project:backend", "due:tomorrow"},
|
||||
checkFn: func(t *testing.T, m *Modifier) {
|
||||
if len(m.SetAttributes) != 3 {
|
||||
t.Errorf("Expected 3 attributes, got %d", len(m.SetAttributes))
|
||||
}
|
||||
if m.SetAttributes["priority"] == nil || *m.SetAttributes["priority"] != "H" {
|
||||
t.Error("Priority not set correctly")
|
||||
}
|
||||
if m.SetAttributes["project"] == nil || *m.SetAttributes["project"] != "backend" {
|
||||
t.Error("Project not set correctly")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "clear attribute",
|
||||
args: []string{"priority:"},
|
||||
checkFn: func(t *testing.T, m *Modifier) {
|
||||
if m.SetAttributes["priority"] != nil {
|
||||
t.Error("Expected priority to be nil (cleared)")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "compound modifier",
|
||||
args: []string{"+urgent", "-someday", "priority:H", "due:tomorrow"},
|
||||
checkFn: func(t *testing.T, m *Modifier) {
|
||||
if len(m.AddTags) != 1 || m.AddTags[0] != "urgent" {
|
||||
t.Error("Add tag not parsed")
|
||||
}
|
||||
if len(m.RemoveTags) != 1 || m.RemoveTags[0] != "someday" {
|
||||
t.Error("Remove tag not parsed")
|
||||
}
|
||||
if len(m.SetAttributes) != 2 {
|
||||
t.Errorf("Expected 2 attributes, got %d", len(m.SetAttributes))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := ParseModifier(tt.args)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseModifier returned error: %v", err)
|
||||
}
|
||||
tt.checkFn(t, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestModifierApply(t *testing.T) {
|
||||
// Create a task
|
||||
task, err := CreateTask("Test task")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create task: %v", err)
|
||||
}
|
||||
|
||||
// Create modifier
|
||||
mod, _ := ParseModifier([]string{"priority:H", "project:backend", "+urgent"})
|
||||
|
||||
// Apply modifier
|
||||
if err := mod.Apply(task); err != nil {
|
||||
t.Fatalf("Failed to apply modifier: %v", err)
|
||||
}
|
||||
|
||||
// Verify changes
|
||||
if task.Priority != PriorityHigh {
|
||||
t.Error("Priority not updated")
|
||||
}
|
||||
|
||||
if task.Project == nil || *task.Project != "backend" {
|
||||
t.Error("Project not updated")
|
||||
}
|
||||
|
||||
// Reload to verify tags were saved
|
||||
reloaded, _ := GetTask(task.UUID)
|
||||
found := false
|
||||
for _, tag := range reloaded.Tags {
|
||||
if tag == "urgent" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Tag not added")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTaskWithModifier(t *testing.T) {
|
||||
mod, _ := ParseModifier([]string{"priority:H", "project:test", "+work", "+urgent"})
|
||||
|
||||
task, err := CreateTaskWithModifier("Task with modifiers", mod)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create task with modifier: %v", err)
|
||||
}
|
||||
|
||||
if task.Priority != PriorityHigh {
|
||||
t.Error("Priority not set during creation")
|
||||
}
|
||||
|
||||
if task.Project == nil || *task.Project != "test" {
|
||||
t.Error("Project not set during creation")
|
||||
}
|
||||
|
||||
if len(task.Tags) != 2 {
|
||||
t.Errorf("Expected 2 tags, got %d", len(task.Tags))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDateISO(t *testing.T) {
|
||||
date, err := ParseDate("2026-01-15")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse ISO date: %v", err)
|
||||
}
|
||||
|
||||
if date.Year() != 2026 || date.Month() != 1 || date.Day() != 15 {
|
||||
t.Errorf("Date not parsed correctly: %v", date)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDateRelative(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
// Test today
|
||||
today, err := ParseDate("today")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse 'today': %v", err)
|
||||
}
|
||||
if today.Day() != now.Day() {
|
||||
t.Error("'today' not parsed correctly")
|
||||
}
|
||||
|
||||
// Test tomorrow
|
||||
tomorrow, err := ParseDate("tomorrow")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse 'tomorrow': %v", err)
|
||||
}
|
||||
expected := now.AddDate(0, 0, 1)
|
||||
if tomorrow.Day() != expected.Day() {
|
||||
t.Error("'tomorrow' not parsed correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDateWeekday(t *testing.T) {
|
||||
// Test Sunday
|
||||
sunday, err := ParseDate("sunday")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse 'sunday': %v", err)
|
||||
}
|
||||
if sunday.Weekday() != time.Sunday {
|
||||
t.Error("'sunday' not parsed correctly")
|
||||
}
|
||||
|
||||
// Test abbreviated form
|
||||
mon, err := ParseDate("mon")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse 'mon': %v", err)
|
||||
}
|
||||
if mon.Weekday() != time.Monday {
|
||||
t.Error("'mon' not parsed correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextWeekday(t *testing.T) {
|
||||
// Test case: Thursday -> next Sunday should be this Sunday
|
||||
thursday := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) // Jan 1, 2026 is a Thursday
|
||||
nextSun := nextWeekday(thursday, time.Sunday)
|
||||
|
||||
if nextSun.Weekday() != time.Sunday {
|
||||
t.Error("Should return Sunday")
|
||||
}
|
||||
|
||||
// Should be 3 days later (this Sunday)
|
||||
expectedDays := 3
|
||||
actualDays := int(nextSun.Sub(thursday).Hours() / 24)
|
||||
if actualDays != expectedDays {
|
||||
t.Errorf("Expected %d days until Sunday, got %d", expectedDays, actualDays)
|
||||
}
|
||||
|
||||
// Test case: Sunday -> next Sunday should be 7 days later
|
||||
sunday := time.Date(2026, 1, 4, 0, 0, 0, 0, time.UTC) // Jan 4, 2026 is a Sunday
|
||||
nextSun2 := nextWeekday(sunday, time.Sunday)
|
||||
|
||||
expectedDays = 7
|
||||
actualDays = int(nextSun2.Sub(sunday).Hours() / 24)
|
||||
if actualDays != expectedDays {
|
||||
t.Errorf("Sunday to Sunday: expected %d days, got %d", expectedDays, actualDays)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModifierWithDates(t *testing.T) {
|
||||
task, _ := CreateTask("Test date modifiers")
|
||||
|
||||
// Apply due date
|
||||
mod, _ := ParseModifier([]string{"due:tomorrow"})
|
||||
if err := mod.Apply(task); err != nil {
|
||||
t.Fatalf("Failed to apply date modifier: %v", err)
|
||||
}
|
||||
|
||||
if task.Due == nil {
|
||||
t.Error("Due date should be set")
|
||||
}
|
||||
|
||||
tomorrow := time.Now().AddDate(0, 0, 1)
|
||||
if task.Due.Day() != tomorrow.Day() {
|
||||
t.Error("Due date not set to tomorrow")
|
||||
}
|
||||
|
||||
// Clear due date
|
||||
mod2, _ := ParseModifier([]string{"due:"})
|
||||
if err := mod2.Apply(task); err != nil {
|
||||
t.Fatalf("Failed to clear due date: %v", err)
|
||||
}
|
||||
|
||||
if task.Due != nil {
|
||||
t.Error("Due date should be cleared")
|
||||
}
|
||||
}
|
||||
|
||||
func TestModifierWithRecurrence(t *testing.T) {
|
||||
task, _ := CreateTask("Test recurrence modifier")
|
||||
|
||||
mod, _ := ParseModifier([]string{"recur:1w"})
|
||||
if err := mod.Apply(task); err != nil {
|
||||
t.Fatalf("Failed to apply recurrence: %v", err)
|
||||
}
|
||||
|
||||
if task.RecurrenceDuration == nil {
|
||||
t.Error("Recurrence should be set")
|
||||
}
|
||||
|
||||
expected := 7 * 24 * time.Hour
|
||||
if *task.RecurrenceDuration != expected {
|
||||
t.Errorf("Expected recurrence %v, got %v", expected, *task.RecurrenceDuration)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user