diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-13 07:11:04 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-13 07:11:04 +0000 |
| commit | 44abf42ed45aa8f285e7ce031cbb9ef1ade667ea (patch) | |
| tree | dbc21cadc2750c6e39e7f7613be0b3f73790c445 /internal/store/native_tasks_test.go | |
| parent | 8310f802dd9fc6ef5dff0be7f640f79c5b39987f (diff) | |
| parent | 4126fe4f56a6eb9703a084d4793a597f37bf2867 (diff) | |
Merge github/master: reconcile with parallel widget work
Another session pushed 27 commits in parallel covering quick-add, event
detail popups, recurrence display, overdue badges, a manual refresh button,
and its own fix for the same overdue-tasks bug (via a separate
GetOverdueNativeTasks fetch folded into BuildTimeline, rather than widening
GetNativeTasksByDateRange's bound directly). Reconciled rather than blindly
taking one side:
- Reverted GetNativeTasksByDateRange to its original bounded query and kept
upstream's GetOverdueNativeTasks + BuildTimeline fold-in as the sole
overdue mechanism for native tasks, to avoid double-counting overdue
items (my widened query + their separate fetch would have both returned
them). Re-pointed the regression test at the now-correct contract and
added a store-level test for GetOverdueNativeTasks directly.
- Kept my GetGoogleTasksByDateRange fix as-is (single unbounded query) --
upstream never touched Google Tasks overdue handling, so there's no
duplication risk there.
- Rewove WidgetRoot's LazyColumn structure (added for scrolling) around
upstream's new header buttons, pinned all-day event rows, and the
enhanced TomorrowSection, none of which were written LazyColumn-aware
since that work landed on this side only.
- Combined both sides' additions to TaskDetailActivity/TaskDetailSheet
(description-edit detail popup + due-date reschedule label) and
WidgetRepository/Actions (optimistic local removal + refresh button
wiring) -- these were independent, non-overlapping features that both
needed to survive.
- Renumbered the migration collision: both sides independently added a
migration numbered 022. Card-description was already applied to the live
production DB under that filename earlier this session (migrations are
tracked by filename), so it keeps 022; the recurring-event-id migration,
never deployed under any name here, moves to 023.
Verified: go build clean, full test suite passes (only the two
pre-existing agent-handler failures and the pre-existing models package
build error remain, both confirmed unrelated via git stash before this
session began), and a dry run against a copy of the live production
database applies both migrations cleanly with no re-run conflicts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
Diffstat (limited to 'internal/store/native_tasks_test.go')
| -rw-r--r-- | internal/store/native_tasks_test.go | 96 |
1 files changed, 96 insertions, 0 deletions
diff --git a/internal/store/native_tasks_test.go b/internal/store/native_tasks_test.go new file mode 100644 index 0000000..5c11d3a --- /dev/null +++ b/internal/store/native_tasks_test.go @@ -0,0 +1,96 @@ +package store + +import ( + "database/sql" + "errors" + "path/filepath" + "testing" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +// newNativeTasksTestStore creates a Store backed by a fresh temp sqlite DB +// with just the native_tasks table -- enough to exercise +// CompleteNativeTask/UncompleteNativeTask/RescheduleNativeTask without +// running the full migration set. +func newNativeTasksTestStore(t *testing.T) *Store { + t.Helper() + dbPath := filepath.Join(t.TempDir(), "test.db") + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { db.Close() }) + if _, err := db.Exec(` + CREATE TABLE native_tasks ( + id TEXT PRIMARY KEY, + content TEXT NOT NULL, + description TEXT DEFAULT '', + project_name TEXT DEFAULT '', + due_date DATETIME, + priority INTEGER DEFAULT 1, + completed BOOLEAN DEFAULT 0, + labels TEXT DEFAULT '[]', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); err != nil { + t.Fatal(err) + } + if _, err := db.Exec(`INSERT INTO native_tasks (id, content) VALUES ('real-1', 'Real task')`); err != nil { + t.Fatal(err) + } + return &Store{db: db} +} + +// TestCompleteNativeTask_UnknownID_ReturnsErrNotFound proves the 2026-07-12 +// fix: a plain UPDATE ... WHERE id = ? silently "succeeds" with a nil error +// when 0 rows match (this is how database/sql's Exec behaves for an UPDATE +// that matches nothing -- no error, just RowsAffected() == 0). Before this +// fix, CompleteNativeTask returned that nil error straight through, so a +// stale/wrong id from a caller (the Android widget, in the real incident +// this was found from) looked identical to a real completion: HTTP 200, +// nothing changed in the database. +func TestCompleteNativeTask_UnknownID_ReturnsErrNotFound(t *testing.T) { + s := newNativeTasksTestStore(t) + + err := s.CompleteNativeTask("does-not-exist") + if !errors.Is(err, ErrNativeTaskNotFound) { + t.Fatalf("expected ErrNativeTaskNotFound, got %v", err) + } +} + +func TestCompleteNativeTask_RealID_Succeeds(t *testing.T) { + s := newNativeTasksTestStore(t) + + if err := s.CompleteNativeTask("real-1"); err != nil { + t.Fatalf("CompleteNativeTask: %v", err) + } + + var completed bool + if err := s.db.QueryRow(`SELECT completed FROM native_tasks WHERE id = 'real-1'`).Scan(&completed); err != nil { + t.Fatal(err) + } + if !completed { + t.Error("expected task to be marked completed") + } +} + +func TestUncompleteNativeTask_UnknownID_ReturnsErrNotFound(t *testing.T) { + s := newNativeTasksTestStore(t) + + err := s.UncompleteNativeTask("does-not-exist") + if !errors.Is(err, ErrNativeTaskNotFound) { + t.Fatalf("expected ErrNativeTaskNotFound, got %v", err) + } +} + +func TestRescheduleNativeTask_UnknownID_ReturnsErrNotFound(t *testing.T) { + s := newNativeTasksTestStore(t) + + err := s.RescheduleNativeTask("does-not-exist", time.Now()) + if !errors.Is(err, ErrNativeTaskNotFound) { + t.Fatalf("expected ErrNativeTaskNotFound, got %v", err) + } +} |
