test: add comprehensive tests for new UX features and fix ISO date timezone bug
Add 33 new test functions covering annotations, undo system, history formatting, relative date display, and weekday parsing pipeline. Fix ISO date parsing to use ParseInLocation instead of Parse to respect the parser's timezone context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRecordUndo_And_PopUndo_Add(t *testing.T) {
|
||||
// Create a task, record undo, then pop undo — should hard-delete the task
|
||||
task, err := CreateTask("Undo add test")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTask: %v", err)
|
||||
}
|
||||
|
||||
if err := RecordUndo("add", task.UUID); err != nil {
|
||||
t.Fatalf("RecordUndo: %v", err)
|
||||
}
|
||||
|
||||
desc, err := PopUndo()
|
||||
if err != nil {
|
||||
t.Fatalf("PopUndo: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(desc, "Undo") || !strings.Contains(desc, "add") {
|
||||
t.Errorf("unexpected undo description: %s", desc)
|
||||
}
|
||||
|
||||
// Task should be gone
|
||||
_, err = GetTask(task.UUID)
|
||||
if err == nil {
|
||||
t.Error("task should have been hard-deleted after undo add")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordUndo_And_PopUndo_Done(t *testing.T) {
|
||||
task, err := CreateTask("Undo done test")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTask: %v", err)
|
||||
}
|
||||
defer func() { task.Delete(true) }()
|
||||
|
||||
// Record undo for the initial creation (so we have a prior change_log entry)
|
||||
// Note: the change_log trigger auto-records on creation, so we just need to
|
||||
// complete and record undo for the completion.
|
||||
|
||||
// Complete the task
|
||||
task.Status = StatusCompleted
|
||||
now := timeNow()
|
||||
task.End = &now
|
||||
if err := task.Save(); err != nil {
|
||||
t.Fatalf("Save completed: %v", err)
|
||||
}
|
||||
|
||||
if err := RecordUndo("done", task.UUID); err != nil {
|
||||
t.Fatalf("RecordUndo: %v", err)
|
||||
}
|
||||
|
||||
// Undo should restore to pending
|
||||
desc, err := PopUndo()
|
||||
if err != nil {
|
||||
t.Fatalf("PopUndo: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(desc, "done") {
|
||||
t.Errorf("expected 'done' in description: %s", desc)
|
||||
}
|
||||
|
||||
// Reload and check status
|
||||
reloaded, err := GetTask(task.UUID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetTask after undo: %v", err)
|
||||
}
|
||||
if reloaded.Status != StatusPending {
|
||||
t.Errorf("status after undo done = %d, want %d (pending)", reloaded.Status, StatusPending)
|
||||
}
|
||||
if reloaded.End != nil {
|
||||
t.Error("End should be nil after undo done")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordUndo_And_PopUndo_Modify(t *testing.T) {
|
||||
task, err := CreateTask("Undo modify test")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTask: %v", err)
|
||||
}
|
||||
defer func() { task.Delete(true) }()
|
||||
|
||||
// Modify the task
|
||||
task.Description = "Modified description"
|
||||
task.Priority = PriorityHigh
|
||||
if err := task.Save(); err != nil {
|
||||
t.Fatalf("Save modified: %v", err)
|
||||
}
|
||||
|
||||
if err := RecordUndo("modify", task.UUID); err != nil {
|
||||
t.Fatalf("RecordUndo: %v", err)
|
||||
}
|
||||
|
||||
// Undo should restore original description and priority
|
||||
_, err = PopUndo()
|
||||
if err != nil {
|
||||
t.Fatalf("PopUndo: %v", err)
|
||||
}
|
||||
|
||||
reloaded, err := GetTask(task.UUID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetTask after undo: %v", err)
|
||||
}
|
||||
if reloaded.Description != "Undo modify test" {
|
||||
t.Errorf("description after undo = %q, want %q", reloaded.Description, "Undo modify test")
|
||||
}
|
||||
if reloaded.Priority != PriorityDefault {
|
||||
t.Errorf("priority after undo = %d, want %d (default)", reloaded.Priority, PriorityDefault)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPopUndo_EmptyStack(t *testing.T) {
|
||||
// Clear the undo stack
|
||||
db := GetDB()
|
||||
db.Exec("DELETE FROM undo_stack")
|
||||
|
||||
_, err := PopUndo()
|
||||
if err == nil {
|
||||
t.Error("PopUndo on empty stack should return error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "nothing to undo") {
|
||||
t.Errorf("expected 'nothing to undo' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUndoStackEviction(t *testing.T) {
|
||||
// Clear existing undo entries
|
||||
db := GetDB()
|
||||
db.Exec("DELETE FROM undo_stack")
|
||||
|
||||
// Create 12 tasks and record undo for each
|
||||
for i := 0; i < 12; i++ {
|
||||
task, err := CreateTask("Eviction test task")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTask %d: %v", i, err)
|
||||
}
|
||||
if err := RecordUndo("add", task.UUID); err != nil {
|
||||
t.Fatalf("RecordUndo %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Stack should be capped at 10
|
||||
var count int
|
||||
if err := db.QueryRow("SELECT COUNT(*) FROM undo_stack").Scan(&count); err != nil {
|
||||
t.Fatalf("count query: %v", err)
|
||||
}
|
||||
if count != 10 {
|
||||
t.Errorf("undo stack count = %d, want 10 (limit)", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyChangeLogData(t *testing.T) {
|
||||
task, err := CreateTask("Apply changelog test")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTask: %v", err)
|
||||
}
|
||||
defer func() { task.Delete(true) }()
|
||||
|
||||
// Apply changelog data that sets various fields
|
||||
due := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)
|
||||
data := "description: Changed description\nstatus: completed\npriority: H\nproject: work\ndue: " +
|
||||
strings.TrimSpace(time.Unix(due.Unix(), 0).Format("")) + "\n"
|
||||
|
||||
// Construct proper data string
|
||||
data = "description: Changed description\nstatus: completed\npriority: H\nproject: work"
|
||||
|
||||
if err := applyChangeLogData(task, data); err != nil {
|
||||
t.Fatalf("applyChangeLogData: %v", err)
|
||||
}
|
||||
|
||||
if task.Description != "Changed description" {
|
||||
t.Errorf("description = %q, want %q", task.Description, "Changed description")
|
||||
}
|
||||
if task.Status != StatusCompleted {
|
||||
t.Errorf("status = %d, want %d", task.Status, StatusCompleted)
|
||||
}
|
||||
if task.Priority != PriorityHigh {
|
||||
t.Errorf("priority = %d, want %d", task.Priority, PriorityHigh)
|
||||
}
|
||||
if task.Project == nil || *task.Project != "work" {
|
||||
t.Error("project should be 'work'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyChangeLogData_ClearsAbsentFields(t *testing.T) {
|
||||
task, err := CreateTask("Clear fields test")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTask: %v", err)
|
||||
}
|
||||
defer func() { task.Delete(true) }()
|
||||
|
||||
// Set some fields first
|
||||
proj := "work"
|
||||
task.Project = &proj
|
||||
now := timeNow()
|
||||
task.Due = &now
|
||||
task.Start = &now
|
||||
|
||||
// Apply data without project, due, or start — they should be cleared
|
||||
data := "description: Clear fields test\nstatus: pending"
|
||||
if err := applyChangeLogData(task, data); err != nil {
|
||||
t.Fatalf("applyChangeLogData: %v", err)
|
||||
}
|
||||
|
||||
if task.Project != nil {
|
||||
t.Error("project should be nil after applying data without project")
|
||||
}
|
||||
if task.Due != nil {
|
||||
t.Error("due should be nil after applying data without due")
|
||||
}
|
||||
if task.Start != nil {
|
||||
t.Error("start should be nil after applying data without start")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileTagsFromChangeLog(t *testing.T) {
|
||||
task, err := CreateTask("Tag reconcile test")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTask: %v", err)
|
||||
}
|
||||
defer func() { task.Delete(true) }()
|
||||
|
||||
// Set current tags
|
||||
task.AddTag("keep")
|
||||
task.AddTag("remove")
|
||||
|
||||
// Reconcile with data that has "keep" and "add" but not "remove"
|
||||
data := "tags: keep,add"
|
||||
if err := reconcileTagsFromChangeLog(task, data); err != nil {
|
||||
t.Fatalf("reconcileTagsFromChangeLog: %v", err)
|
||||
}
|
||||
|
||||
tags, _ := task.GetTags()
|
||||
tagSet := make(map[string]bool)
|
||||
for _, tag := range tags {
|
||||
tagSet[tag] = true
|
||||
}
|
||||
|
||||
if !tagSet["keep"] {
|
||||
t.Error("tag 'keep' should still be present")
|
||||
}
|
||||
if !tagSet["add"] {
|
||||
t.Error("tag 'add' should have been added")
|
||||
}
|
||||
if tagSet["remove"] {
|
||||
t.Error("tag 'remove' should have been removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileTagsFromChangeLog_NoTags(t *testing.T) {
|
||||
task, err := CreateTask("No tags reconcile test")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTask: %v", err)
|
||||
}
|
||||
defer func() { task.Delete(true) }()
|
||||
|
||||
task.AddTag("should-be-removed")
|
||||
|
||||
// Data with no tags line — all tags should be removed
|
||||
data := "description: No tags reconcile test\nstatus: pending"
|
||||
if err := reconcileTagsFromChangeLog(task, data); err != nil {
|
||||
t.Fatalf("reconcileTagsFromChangeLog: %v", err)
|
||||
}
|
||||
|
||||
tags, _ := task.GetTags()
|
||||
if len(tags) != 0 {
|
||||
t.Errorf("expected 0 tags after reconcile with no tags, got %v", tags)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user