Files
gems/opal-task/internal/engine/modifier_test.go
T
joakim c99a4a2d95 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
2026-01-04 14:48:43 +01:00

272 lines
6.7 KiB
Go

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)
}
}