From 945c34590677e161a711ccaff0e1077197b1b178 Mon Sep 17 00:00:00 2001 From: Doot Agent Date: Mon, 6 Jul 2026 00:00:59 +0000 Subject: feat: remove Todoist integration entirely Native tasks (native_tasks table) fully replace Todoist. All Todoist API code, store functions, handlers, routes, templates, and tests have been removed. Migration 021 drops the now-unused tasks cache table. Co-Authored-By: Claude Sonnet 4.6 --- internal/handlers/agent.go | 38 +- internal/handlers/agent_test.go | 26 +- internal/handlers/atoms.go | 14 +- internal/handlers/handlers.go | 252 +--------- internal/handlers/handlers_test.go | 802 ++----------------------------- internal/handlers/settings.go | 25 +- internal/handlers/tab_state_test.go | 7 +- internal/handlers/timeline_logic.go | 49 +- internal/handlers/timeline_logic_test.go | 146 +----- internal/handlers/widget.go | 11 +- internal/handlers/widget_test.go | 8 +- 11 files changed, 96 insertions(+), 1282 deletions(-) (limited to 'internal/handlers') diff --git a/internal/handlers/agent.go b/internal/handlers/agent.go index 826ffd7..073084f 100644 --- a/internal/handlers/agent.go +++ b/internal/handlers/agent.go @@ -414,12 +414,6 @@ func (h *Handler) handleAgentTaskToggle(w http.ResponseWriter, r *http.Request, var err error ctx := r.Context() switch source { - case "todoist": - if complete { - err = h.todoistClient.CompleteTask(ctx, id) - } else { - err = h.todoistClient.ReopenTask(ctx, id) - } case "trello": err = h.trelloClient.UpdateCard(ctx, id, map[string]interface{}{"closed": complete}) case "gtasks": @@ -450,17 +444,11 @@ func (h *Handler) handleAgentTaskToggle(w http.ResponseWriter, r *http.Request, if complete { title, dueDate := h.getAtomDetails(id, source) _ = h.store.SaveCompletedTask(source, id, title, dueDate) - switch source { - case "todoist": - _ = h.store.DeleteTask(id) - case "trello": + if source == "trello" { _ = h.store.DeleteCard(id) } } else { - switch source { - case "todoist": - _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks) - case "trello": + if source == "trello" { _ = h.store.InvalidateCache(store.CacheKeyTrelloBoards) } } @@ -485,8 +473,6 @@ func (h *Handler) HandleAgentTaskUpdateDue(w http.ResponseWriter, r *http.Reques var err error ctx := r.Context() switch source { - case "todoist": - err = h.todoistClient.UpdateTask(ctx, id, map[string]interface{}{"due_datetime": req.Due}) case "trello": err = h.trelloClient.UpdateCard(ctx, id, map[string]interface{}{"due": req.Due}) default: @@ -500,10 +486,7 @@ func (h *Handler) HandleAgentTaskUpdateDue(w http.ResponseWriter, r *http.Reques } // Invalidate cache - switch source { - case "todoist": - _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks) - case "trello": + if source == "trello" { _ = h.store.InvalidateCache(store.CacheKeyTrelloBoards) } @@ -525,13 +508,6 @@ func (h *Handler) HandleAgentTaskUpdate(w http.ResponseWriter, r *http.Request) var err error ctx := r.Context() switch source { - case "todoist": - // Map generic updates to Todoist specific ones if needed - if title, ok := updates["title"].(string); ok { - updates["content"] = title - delete(updates, "title") - } - err = h.todoistClient.UpdateTask(ctx, id, updates) case "trello": if title, ok := updates["title"].(string); ok { updates["name"] = title @@ -553,10 +529,7 @@ func (h *Handler) HandleAgentTaskUpdate(w http.ResponseWriter, r *http.Request) } // Invalidate cache - switch source { - case "todoist": - _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks) - case "trello": + if source == "trello" { _ = h.store.InvalidateCache(store.CacheKeyTrelloBoards) } @@ -587,9 +560,6 @@ func (h *Handler) HandleAgentTaskCreate(w http.ResponseWriter, r *http.Request) var err error ctx := r.Context() switch req.Source { - case "todoist": - _, err = h.todoistClient.CreateTask(ctx, req.Title, req.ProjectID, req.DueDate, req.Priority) - _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks) case "trello": if req.ListID == "" { http.Error(w, "list_id is required for Trello", http.StatusBadRequest) diff --git a/internal/handlers/agent_test.go b/internal/handlers/agent_test.go index eab1609..2893a13 100644 --- a/internal/handlers/agent_test.go +++ b/internal/handlers/agent_test.go @@ -746,13 +746,11 @@ func TestHandleAgentTaskWriteOperations(t *testing.T) { defer cleanup() // Mock clients - mockTodoist := &mockTodoistClient{} mockTrello := &mockTrelloClient{} h := &Handler{ - store: db, - todoistClient: mockTodoist, - trelloClient: mockTrello, - config: &config.Config{}, + store: db, + trelloClient: mockTrello, + config: &config.Config{}, } // Create an approved session @@ -766,11 +764,11 @@ func TestHandleAgentTaskWriteOperations(t *testing.T) { db.CreateAgentSession(session) db.ApproveAgentSession("write-request-token", sessionToken, time.Now().Add(1*time.Hour)) - // Pre-populate store with a task to get details during completion - db.SaveTasks([]models.Task{{ID: "task123", Content: "Test Task"}}) + // Pre-populate store with a native task + _ = db.CreateNativeTask(models.Task{ID: "task123", Content: "Test Task"}) t.Run("complete task", func(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "/agent/tasks/task123/complete?source=todoist", nil) + req := httptest.NewRequest(http.MethodPost, "/agent/tasks/task123/complete?source=doot", nil) w := httptest.NewRecorder() // Manually inject session into context as middleware would @@ -804,7 +802,7 @@ func TestHandleAgentTaskWriteOperations(t *testing.T) { t.Run("update due date", func(t *testing.T) { due := time.Now().Add(24 * time.Hour) body, _ := json.Marshal(map[string]*time.Time{"due": &due}) - req := httptest.NewRequest(http.MethodPatch, "/agent/tasks/task123/due?source=todoist", bytes.NewReader(body)) + req := httptest.NewRequest(http.MethodPatch, "/agent/tasks/task123/due?source=doot", bytes.NewReader(body)) w := httptest.NewRecorder() rctx := chi.NewRouteContext() @@ -820,7 +818,7 @@ func TestHandleAgentTaskWriteOperations(t *testing.T) { t.Run("update task details", func(t *testing.T) { body, _ := json.Marshal(map[string]string{"title": "Updated Title", "description": "New Desc"}) - req := httptest.NewRequest(http.MethodPatch, "/agent/tasks/task123?source=todoist", bytes.NewReader(body)) + req := httptest.NewRequest(http.MethodPatch, "/agent/tasks/task123?source=doot", bytes.NewReader(body)) w := httptest.NewRecorder() rctx := chi.NewRouteContext() @@ -839,17 +837,15 @@ func TestHandleAgentCreateOperations(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() - mockTodoist := &mockTodoistClient{} h := &Handler{ - store: db, - todoistClient: mockTodoist, - config: &config.Config{}, + store: db, + config: &config.Config{}, } t.Run("create task", func(t *testing.T) { body, _ := json.Marshal(map[string]string{ "title": "New Agent Task", - "source": "todoist", + "source": "doot", }) req := httptest.NewRequest(http.MethodPost, "/agent/tasks", bytes.NewReader(body)) w := httptest.NewRecorder() diff --git a/internal/handlers/atoms.go b/internal/handlers/atoms.go index 0b59060..fe092a3 100644 --- a/internal/handlers/atoms.go +++ b/internal/handlers/atoms.go @@ -12,11 +12,6 @@ import ( // BuildUnifiedAtomList creates a list of atoms from tasks, cards, and google tasks func BuildUnifiedAtomList(s *store.Store, claudomator api.ClaudomatorClient) ([]models.Atom, []models.Board, error) { - tasks, err := s.GetTasks() - if err != nil { - return nil, nil, err - } - boards, err := s.GetBoards() if err != nil { return nil, nil, err @@ -33,20 +28,13 @@ func BuildUnifiedAtomList(s *store.Store, claudomator api.ClaudomatorClient) ([] log.Printf("Warning: failed to fetch native tasks: %v", err) } - atoms := make([]models.Atom, 0, len(tasks)+len(gTasks)+len(nativeTasks)) + atoms := make([]models.Atom, 0, len(gTasks)+len(nativeTasks)) // Add native doot tasks (GetNativeTasks already filters completed=0) for _, task := range nativeTasks { atoms = append(atoms, models.NativeTaskToAtom(task)) } - // Add incomplete tasks - for _, task := range tasks { - if !task.Completed { - atoms = append(atoms, models.TaskToAtom(task)) - } - } - // Add incomplete google tasks for _, gTask := range gTasks { if !gTask.Completed { diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 6c74fff..d1a7512 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -30,7 +30,6 @@ func newID() string { // Handler holds dependencies for HTTP handlers type Handler struct { store *store.Store - todoistClient api.TodoistAPI trelloClient api.TrelloAPI planToEatClient api.PlanToEatAPI googleCalendarClient api.GoogleCalendarAPI @@ -43,7 +42,7 @@ type Handler struct { } // New creates a new Handler instance -func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat api.PlanToEatAPI, googleCalendar api.GoogleCalendarAPI, googleTasks api.GoogleTasksAPI, claudomator api.ClaudomatorClient, cfg *config.Config, buildVersion string, webAuthnEnabled bool) *Handler { +func New(s *store.Store, trello api.TrelloAPI, planToEat api.PlanToEatAPI, googleCalendar api.GoogleCalendarAPI, googleTasks api.GoogleTasksAPI, claudomator api.ClaudomatorClient, cfg *config.Config, buildVersion string, webAuthnEnabled bool) *Handler { // Template functions funcMap := template.FuncMap{ "subtract": func(a, b int) int { return a - b }, @@ -63,7 +62,6 @@ func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat return &Handler{ store: s, - todoistClient: todoist, trelloClient: trello, planToEatClient: planToEat, googleCalendarClient: googleCalendar, @@ -134,16 +132,6 @@ func (h *Handler) HandleRefresh(w http.ResponseWriter, r *http.Request) { JSONResponse(w, data) } -// HandleGetTasks returns tasks as JSON -func (h *Handler) HandleGetTasks(w http.ResponseWriter, r *http.Request) { - tasks, err := h.store.GetTasks() - if err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to get tasks", err) - return - } - JSONResponse(w, tasks) -} - // HandleGetMeals returns meals as JSON func (h *Handler) HandleGetMeals(w http.ResponseWriter, r *http.Request) { startDate := config.Now() @@ -221,17 +209,6 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models return err }) - fetch("Todoist", func() error { - tasks, err := h.fetchTasks(ctx, forceRefresh) - if err == nil { - sortTasksByUrgency(tasks) - mu.Lock() - data.Tasks = tasks - mu.Unlock() - } - return err - }) - if h.planToEatClient != nil { fetch("PlanToEat", func() error { meals, err := h.fetchMeals(ctx, forceRefresh) @@ -270,35 +247,12 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models wg.Wait() - // Populate projects from cached tasks (avoids deprecated REST API) - if projects, err := h.store.GetProjectsFromTasks(); err == nil { - data.Projects = projects - } - // Extract and sort Trello tasks data.TrelloTasks = filterAndSortTrelloTasks(data.Boards) return data, nil } -// sortTasksByUrgency sorts tasks by due date then priority -func sortTasksByUrgency(tasks []models.Task) { - sort.Slice(tasks, func(i, j int) bool { - if tasks[i].DueDate == nil && tasks[j].DueDate != nil { - return false - } - if tasks[i].DueDate != nil && tasks[j].DueDate == nil { - return true - } - if tasks[i].DueDate != nil && tasks[j].DueDate != nil { - if !tasks[i].DueDate.Equal(*tasks[j].DueDate) { - return tasks[i].DueDate.Before(*tasks[j].DueDate) - } - } - return tasks[i].Priority > tasks[j].Priority - }) -} - // filterAndSortTrelloTasks extracts task-like cards from boards func filterAndSortTrelloTasks(boards []models.Board) []models.Card { var tasks []models.Card @@ -328,39 +282,6 @@ func filterAndSortTrelloTasks(boards []models.Board) []models.Card { return tasks } -// fetchTasks fetches tasks from cache or API -func (h *Handler) fetchTasks(ctx context.Context, forceRefresh bool) ([]models.Task, error) { - cacheKey := store.CacheKeyTodoistTasks - - // Check cache validity - if !forceRefresh { - valid, err := h.store.IsCacheValid(cacheKey) - if err == nil && valid { - return h.store.GetTasks() - } - } - - tasks, err := h.todoistClient.GetTasks(ctx) - if err != nil { - // Fall back to cached data if available - cachedTasks, cacheErr := h.store.GetTasks() - if cacheErr == nil && len(cachedTasks) > 0 { - return cachedTasks, nil - } - return nil, err - } - - if err := h.store.SaveTasks(tasks); err != nil { - log.Printf("Failed to save tasks to cache: %v", err) - } - - if err := h.store.UpdateCacheMetadata(cacheKey, h.config.CacheTTLMinutes); err != nil { - log.Printf("Failed to update cache metadata: %v", err) - } - - return tasks, nil -} - // fetchMeals fetches meals from cache or API func (h *Handler) fetchMeals(ctx context.Context, forceRefresh bool) ([]models.Meal, error) { startDate := config.Now() @@ -534,52 +455,6 @@ func (h *Handler) HandleCompleteCard(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -// HandleCreateTask creates a new Todoist task -func (h *Handler) HandleCreateTask(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - content, ok := requireFormValue(w, r, "content") - if !ok { - return - } - projectID := r.FormValue("project_id") - - if _, err := h.todoistClient.CreateTask(ctx, content, projectID, nil, 0); err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to create task", err) - return - } - - tasks, err := h.fetchTasks(ctx, true) - if err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to refresh tasks", err) - return - } - - projects, _ := h.store.GetProjectsFromTasks() - - data := struct { - Tasks []models.Task - Projects []models.Project - }{Tasks: tasks, Projects: projects} - - HTMLResponse(w, h.renderer, "todoist-tasks", data) -} - -// HandleCompleteTask marks a Todoist task as complete -func (h *Handler) HandleCompleteTask(w http.ResponseWriter, r *http.Request) { - taskID, ok := requireFormValue(w, r, "task_id") - if !ok { - return - } - - if err := h.todoistClient.CompleteTask(r.Context(), taskID); err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to complete task", err) - return - } - - w.WriteHeader(http.StatusOK) -} - // HandleCompleteAtom handles completion of a unified task (Atom) func (h *Handler) HandleCompleteAtom(w http.ResponseWriter, r *http.Request) { h.handleAtomToggle(w, r, true) @@ -623,16 +498,6 @@ func (h *Handler) handleAtomToggle(w http.ResponseWriter, r *http.Request, compl } else { err = h.store.UncompleteNativeTask(id) } - case "todoist": - if h.todoistClient == nil { - JSONError(w, http.StatusServiceUnavailable, "Todoist not configured", nil) - return - } - if complete { - err = h.todoistClient.CompleteTask(ctx, id) - } else { - err = h.todoistClient.ReopenTask(ctx, id) - } case "trello": err = h.trelloClient.UpdateCard(ctx, id, map[string]interface{}{"closed": complete}) case "gtasks": @@ -674,8 +539,6 @@ func (h *Handler) handleAtomToggle(w http.ResponseWriter, r *http.Request, compl // Remove from local cache switch source { - case "todoist": - _ = h.store.DeleteTask(id) case "trello": _ = h.store.DeleteCard(id) case "doot": @@ -691,10 +554,7 @@ func (h *Handler) handleAtomToggle(w http.ResponseWriter, r *http.Request, compl HTMLResponse(w, h.renderer, "completed-atom", data) } else { // Invalidate cache to force refresh - switch source { - case "todoist": - _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks) - case "trello": + if source == "trello" { _ = h.store.InvalidateCache(store.CacheKeyTrelloBoards) } // Don't swap empty response - just trigger refresh @@ -715,14 +575,6 @@ func (h *Handler) getAtomDetails(id, source string) (string, *time.Time) { } } } - case "todoist": - if tasks, err := h.store.GetTasks(); err == nil { - for _, t := range tasks { - if t.ID == id { - return t.Content, t.DueDate - } - } - } case "trello": if boards, err := h.store.GetBoards(); err == nil { for _, b := range boards { @@ -739,7 +591,7 @@ func (h *Handler) getAtomDetails(id, source string) (string, *time.Time) { return "Task", nil } -// HandleUnifiedAdd creates a task in Todoist or a card in Trello from the Quick Add form +// HandleUnifiedAdd creates a native task or a card in Trello from the Quick Add form func (h *Handler) HandleUnifiedAdd(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -767,25 +619,17 @@ func (h *Handler) HandleUnifiedAdd(w http.ResponseWriter, r *http.Request) { switch source { case "doot": task := models.Task{ - ID: newID(), - Content: title, - DueDate: dueDate, - Priority: 1, - Labels: nil, + ID: newID(), + Content: title, + DueDate: dueDate, + Priority: 1, + Labels: nil, } if err := h.store.CreateNativeTask(task); err != nil { JSONError(w, http.StatusInternalServerError, "Failed to create task", err) return } - case "todoist": - projectID := r.FormValue("project_id") - if _, err := h.todoistClient.CreateTask(ctx, title, projectID, dueDate, 1); err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to create Todoist task", err) - return - } - _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks) - case "trello": listID := r.FormValue("list_id") if listID == "" { @@ -836,15 +680,6 @@ func (h *Handler) HandleGetTaskDetail(w http.ResponseWriter, r *http.Request) { var title, description string switch source { - case "todoist": - if tasks, err := h.store.GetTasks(); err == nil { - for _, t := range tasks { - if t.ID == id { - title, description = t.Content, t.Description - break - } - } - } case "trello": if boards, err := h.store.GetBoards(); err == nil { for _, b := range boards { @@ -887,15 +722,6 @@ func (h *Handler) HandleTaskDetailPage(w http.ResponseWriter, r *http.Request) { } } } - case "todoist": - if tasks, err := h.store.GetTasks(); err == nil { - for _, t := range tasks { - if t.ID == id { - title, description = t.Content, t.Description - break - } - } - } case "trello": if boards, err := h.store.GetBoards(); err == nil { for _, b := range boards { @@ -947,8 +773,6 @@ func (h *Handler) HandleUpdateTask(w http.ResponseWriter, r *http.Request) { switch source { case "doot": err = h.store.UpdateNativeTask(id, "", description) - case "todoist": - err = h.todoistClient.UpdateTask(r.Context(), id, map[string]interface{}{"description": description}) case "trello": err = h.trelloClient.UpdateCard(r.Context(), id, map[string]interface{}{"desc": description}) default: @@ -968,18 +792,7 @@ func (h *Handler) HandleUpdateTask(w http.ResponseWriter, r *http.Request) { } } -// HandleImportFromTodoist copies the current Todoist task cache into native_tasks. -// One-time migration action. Safe to run multiple times (INSERT OR IGNORE by ID). -func (h *Handler) HandleImportFromTodoist(w http.ResponseWriter, r *http.Request) { - count, err := h.store.ImportFromTodoist() - if err != nil { - http.Error(w, "Import failed: "+err.Error(), http.StatusInternalServerError) - return - } - fmt.Fprintf(w, "Imported %d tasks from Todoist. You can now remove TODOIST_TOKEN from your .env to disable Todoist.", count) -} - -// HandleTabTasks renders the unified Tasks tab (Todoist + Trello cards with due dates + Bugs + Google Tasks) +// HandleTabTasks renders the unified Tasks tab (native tasks + Trello cards with due dates + Google Tasks) func (h *Handler) HandleTabTasks(w http.ResponseWriter, r *http.Request) { atoms, boards, err := BuildUnifiedAtomList(h.store, h.claudomatorClient) if err != nil { @@ -990,19 +803,15 @@ func (h *Handler) HandleTabTasks(w http.ResponseWriter, r *http.Request) { SortAtomsByUrgency(atoms) currentAtoms, futureAtoms := PartitionAtomsByTime(atoms) - projects, _ := h.store.GetProjectsFromTasks() - data := struct { Atoms []models.Atom FutureAtoms []models.Atom Boards []models.Board - Projects []models.Project Today string }{ Atoms: currentAtoms, FutureAtoms: futureAtoms, Boards: boards, - Projects: projects, Today: config.Now().Format("2006-01-02"), } @@ -1016,7 +825,6 @@ func (h *Handler) HandleTabPlanning(w http.ResponseWriter, r *http.Request) { in3Days := today.AddDate(0, 0, 4) boards, _ := h.store.GetBoards() - tasks, _ := h.store.GetTasks() gTasks, _ := h.store.GetGoogleTasks() events, _ := h.fetchCalendarEvents(r.Context(), false) @@ -1044,44 +852,6 @@ func (h *Handler) HandleTabPlanning(w http.ResponseWriter, r *http.Request) { } } - for _, task := range tasks { - if task.Completed || task.DueDate == nil { - continue - } - dueDate := *task.DueDate - hasTime := dueDate.Hour() != 0 || dueDate.Minute() != 0 - - if dueDate.Before(tomorrow) { - if hasTime { - scheduled = append(scheduled, ScheduledItem{ - Type: "task", - ID: task.ID, - Title: task.Content, - Start: dueDate, - URL: task.URL, - Source: "todoist", - SourceIcon: "✓", - Priority: task.Priority, - }) - } else { - atom := models.TaskToAtom(task) - atom.ComputeUIFields() - unscheduled = append(unscheduled, atom) - } - } else if dueDate.Before(in3Days) { - upcoming = append(upcoming, ScheduledItem{ - Type: "task", - ID: task.ID, - Title: task.Content, - Start: dueDate, - URL: task.URL, - Source: "todoist", - SourceIcon: "✓", - Priority: task.Priority, - }) - } - } - for _, board := range boards { for _, card := range board.Cards { if card.DueDate == nil { @@ -1171,21 +941,17 @@ func (h *Handler) HandleTabPlanning(w http.ResponseWriter, r *http.Request) { sort.Slice(unscheduled, func(i, j int) bool { return unscheduled[i].Priority > unscheduled[j].Priority }) sort.Slice(upcoming, func(i, j int) bool { return upcoming[i].Start.Before(upcoming[j].Start) }) - projects, _ := h.store.GetProjectsFromTasks() - data := struct { Scheduled []ScheduledItem Unscheduled []models.Atom Upcoming []ScheduledItem Boards []models.Board - Projects []models.Project Today string }{ Scheduled: scheduled, Unscheduled: unscheduled, Upcoming: upcoming, Boards: boards, - Projects: projects, Today: today.Format("2006-01-02"), } diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index b640c78..1f66378 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -84,40 +84,6 @@ func setupTestHandler(t *testing.T) (*Handler, func()) { return h, cleanup } -// mockTodoistClient creates a mock Todoist client for testing -type mockTodoistClient struct { - tasks []models.Task - err error -} - -func (m *mockTodoistClient) GetTasks(ctx context.Context) ([]models.Task, error) { - if m.err != nil { - return nil, m.err - } - return m.tasks, nil -} - -func (m *mockTodoistClient) GetProjects(ctx context.Context) ([]models.Project, error) { - return []models.Project{}, nil -} - -func (m *mockTodoistClient) CreateTask(ctx context.Context, content, projectID string, dueDate *time.Time, priority int) (*models.Task, error) { - return nil, nil -} - -func (m *mockTodoistClient) UpdateTask(ctx context.Context, taskID string, updates map[string]interface{}) error { - return m.err -} - -func (m *mockTodoistClient) CompleteTask(ctx context.Context, taskID string) error { - return nil -} - -func (m *mockTodoistClient) ReopenTask(ctx context.Context, taskID string) error { - return nil -} - - // mockTrelloClient creates a mock Trello client for testing type mockTrelloClient struct { boards []models.Board @@ -154,79 +120,6 @@ func (m *mockTrelloClient) UpdateCard(ctx context.Context, cardID string, update return nil } -// TestHandleGetTasks tests the HandleGetTasks handler -func TestHandleGetTasks(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - // Create test tasks - testTasks := []models.Task{ - { - ID: "1", - Content: "Test task 1", - Description: "Description 1", - ProjectID: "proj1", - ProjectName: "Project 1", - Priority: 1, - Completed: false, - Labels: []string{"label1"}, - URL: "https://todoist.com/task/1", - CreatedAt: time.Now(), - }, - { - ID: "2", - Content: "Test task 2", - Description: "Description 2", - ProjectID: "proj2", - ProjectName: "Project 2", - Priority: 2, - Completed: true, - Labels: []string{"label2"}, - URL: "https://todoist.com/task/2", - CreatedAt: time.Now(), - }, - } - - // Save tasks to database - if err := db.SaveTasks(testTasks); err != nil { - t.Fatalf("Failed to save test tasks: %v", err) - } - - // Create handler with mock client - cfg := &config.Config{ - CacheTTLMinutes: 5, - } - mockTodoist := &mockTodoistClient{} - h := &Handler{ - store: db, - todoistClient: mockTodoist, - config: cfg, - } - - // Create test request - req := httptest.NewRequest("GET", "/api/tasks", nil) - w := httptest.NewRecorder() - - // Execute handler - h.HandleGetTasks(w, req) - - // Check response - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - // Parse response - var tasks []models.Task - if err := json.NewDecoder(w.Body).Decode(&tasks); err != nil { - t.Fatalf("Failed to decode response: %v", err) - } - - // Verify tasks - if len(tasks) != 2 { - t.Errorf("Expected 2 tasks, got %d", len(tasks)) - } -} - // TestHandleGetBoards tests the HandleGetBoards handler func TestHandleGetBoards(t *testing.T) { db, cleanup := setupTestDB(t) @@ -309,16 +202,6 @@ func TestHandleRefresh(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() - // Create mock clients - mockTodoist := &mockTodoistClient{ - tasks: []models.Task{ - { - ID: "1", - Content: "Test task", - }, - }, - } - mockTrello := &mockTrelloClient{ boards: []models.Board{ { @@ -333,10 +216,9 @@ func TestHandleRefresh(t *testing.T) { CacheTTLMinutes: 5, } h := &Handler{ - store: db, - todoistClient: mockTodoist, - trelloClient: mockTrello, - config: cfg, + store: db, + trelloClient: mockTrello, + config: cfg, } // Create test request @@ -397,21 +279,6 @@ func TestHandleGetMeals(t *testing.T) { } } -// mockTodoistClientWithComplete tracks CompleteTask calls -type mockTodoistClientWithComplete struct { - mockTodoistClient - completedTaskIDs []string - completeErr error -} - -func (m *mockTodoistClientWithComplete) CompleteTask(ctx context.Context, taskID string) error { - if m.completeErr != nil { - return m.completeErr - } - m.completedTaskIDs = append(m.completedTaskIDs, taskID) - return nil -} - // mockTrelloClientWithUpdate tracks UpdateCard calls type mockTrelloClientWithUpdate struct { mockTrelloClient @@ -427,138 +294,6 @@ func (m *mockTrelloClientWithUpdate) UpdateCard(ctx context.Context, cardID stri return nil } -// TestHandleCompleteAtom_Todoist tests completing a Todoist task -func TestHandleCompleteAtom_Todoist(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - // Save a task to the cache - tasks := []models.Task{ - { - ID: "task123", - Content: "Test task", - Completed: false, - Labels: []string{}, - CreatedAt: time.Now(), - }, - } - if err := db.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to save test task: %v", err) - } - - // Verify task exists in cache - cachedTasks, _ := db.GetTasks() - if len(cachedTasks) != 1 { - t.Fatalf("Expected 1 task in cache, got %d", len(cachedTasks)) - } - - // Create handler with mock client - mockTodoist := &mockTodoistClientWithComplete{} - h := &Handler{ - store: db, - todoistClient: mockTodoist, - config: &config.Config{}, - renderer: newTestRenderer(), - } - - // Create request - req := httptest.NewRequest("POST", "/complete-atom", nil) - req.Form = map[string][]string{ - "id": {"task123"}, - "source": {"todoist"}, - } - w := httptest.NewRecorder() - - // Execute handler - h.HandleCompleteAtom(w, req) - - // Check response - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - // Verify response body contains rendered completed-atom template - body := w.Body.String() - if !strings.Contains(body, "rendered:completed-atom") { - t.Errorf("Expected response body to contain 'rendered:completed-atom', got %q", body) - } - - // Verify Content-Type header - ct := w.Header().Get("Content-Type") - if ct != "text/html; charset=utf-8" { - t.Errorf("Expected Content-Type 'text/html; charset=utf-8', got %q", ct) - } - - // Verify CompleteTask was called on the API - if len(mockTodoist.completedTaskIDs) != 1 || mockTodoist.completedTaskIDs[0] != "task123" { - t.Errorf("Expected CompleteTask to be called with 'task123', got %v", mockTodoist.completedTaskIDs) - } - - // Verify task was deleted from cache - cachedTasks, _ = db.GetTasks() - if len(cachedTasks) != 0 { - t.Errorf("Expected task to be deleted from cache, but found %d tasks", len(cachedTasks)) - } -} - -func TestHandleCompleteAtom_RendersCorrectTemplateData(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - // Seed a task with known title - tasks := []models.Task{ - {ID: "task-data-1", Content: "Buy groceries", Labels: []string{}, CreatedAt: time.Now()}, - } - if err := db.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to seed: %v", err) - } - - renderer := NewMockRenderer() - h := &Handler{ - store: db, - todoistClient: &mockTodoistClientWithComplete{}, - config: &config.Config{}, - renderer: renderer, - } - - req := httptest.NewRequest("POST", "/complete-atom", nil) - req.Form = map[string][]string{"id": {"task-data-1"}, "source": {"todoist"}} - w := httptest.NewRecorder() - - h.HandleCompleteAtom(w, req) - - // Verify renderer was called with "completed-atom" and correct data - if len(renderer.Calls) != 1 { - t.Fatalf("Expected 1 render call, got %d", len(renderer.Calls)) - } - call := renderer.Calls[0] - if call.Name != "completed-atom" { - t.Errorf("Expected template 'completed-atom', got %q", call.Name) - } - - // The data should contain the task ID, source, and title - type atomData struct { - ID string - Source string - Title string - } - // Use JSON round-trip to extract fields from the anonymous struct - jsonBytes, _ := json.Marshal(call.Data) - var got atomData - if err := json.Unmarshal(jsonBytes, &got); err != nil { - t.Fatalf("Failed to unmarshal render data: %v", err) - } - if got.ID != "task-data-1" { - t.Errorf("Expected data.ID 'task-data-1', got %q", got.ID) - } - if got.Source != "todoist" { - t.Errorf("Expected data.Source 'todoist', got %q", got.Source) - } - if got.Title != "Buy groceries" { - t.Errorf("Expected data.Title 'Buy groceries', got %q", got.Title) - } -} - // TestHandleCompleteAtom_Trello tests completing a Trello card func TestHandleCompleteAtom_Trello(t *testing.T) { db, cleanup := setupTestDB(t) @@ -641,7 +376,7 @@ func TestHandleCompleteAtom_MissingParams(t *testing.T) { id string source string }{ - {"missing id", "", "todoist"}, + {"missing id", "", "doot"}, {"missing source", "task123", ""}, {"both missing", "", ""}, } @@ -688,56 +423,6 @@ func TestHandleCompleteAtom_UnknownSource(t *testing.T) { } } -// TestHandleCompleteAtom_APIError tests that API errors are handled correctly -func TestHandleCompleteAtom_APIError(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - // Save a task to the cache - tasks := []models.Task{ - { - ID: "task123", - Content: "Test task", - Completed: false, - Labels: []string{}, - CreatedAt: time.Now(), - }, - } - if err := db.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to save test task: %v", err) - } - - // Create handler with mock client that returns an error - mockTodoist := &mockTodoistClientWithComplete{ - completeErr: context.DeadlineExceeded, - } - h := &Handler{ - store: db, - todoistClient: mockTodoist, - config: &config.Config{}, - } - - req := httptest.NewRequest("POST", "/complete-atom", nil) - req.Form = map[string][]string{ - "id": {"task123"}, - "source": {"todoist"}, - } - w := httptest.NewRecorder() - - h.HandleCompleteAtom(w, req) - - // Should return 500 on API error - if w.Code != http.StatusInternalServerError { - t.Errorf("Expected status 500 on API error, got %d", w.Code) - } - - // Verify task was NOT deleted from cache (rollback behavior) - cachedTasks, _ := db.GetTasks() - if len(cachedTasks) != 1 { - t.Errorf("Task should NOT be deleted from cache on API error, found %d tasks", len(cachedTasks)) - } -} - // TestHandleShoppingModeComplete tests completing (removing) shopping items func TestHandleShoppingModeComplete(t *testing.T) { db, cleanup := setupTestDB(t) @@ -1354,12 +1039,6 @@ func TestHandleTabTasks(t *testing.T) { config: &config.Config{CacheTTLMinutes: 5}, } - // Add some tasks - tasks := []models.Task{ - {ID: "1", Content: "Task 1", Labels: []string{}, CreatedAt: time.Now()}, - } - _ = db.SaveTasks(tasks) - req := httptest.NewRequest("GET", "/tabs/tasks", nil) w := httptest.NewRecorder() @@ -1599,69 +1278,6 @@ func TestHTMLString(t *testing.T) { // Uncomplete Atom Tests // ============================================================================= -// mockTodoistClientWithReopen tracks ReopenTask calls -type mockTodoistClientWithReopen struct { - mockTodoistClient - reopenedTaskIDs []string - reopenErr error -} - -func (m *mockTodoistClientWithReopen) ReopenTask(ctx context.Context, taskID string) error { - if m.reopenErr != nil { - return m.reopenErr - } - m.reopenedTaskIDs = append(m.reopenedTaskIDs, taskID) - return nil -} - -func TestHandleUncompleteAtom_Todoist(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - mockTodoist := &mockTodoistClientWithReopen{} - h := &Handler{ - store: db, - todoistClient: mockTodoist, - config: &config.Config{}, - renderer: newTestRenderer(), - } - - req := httptest.NewRequest("POST", "/uncomplete-atom", nil) - req.Form = map[string][]string{ - "id": {"task123"}, - "source": {"todoist"}, - } - w := httptest.NewRecorder() - - h.HandleUncompleteAtom(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected status 200, got %d", w.Code) - } - - // Verify HX-Reswap header set to "none" - reswap := w.Header().Get("HX-Reswap") - if reswap != "none" { - t.Errorf("Expected HX-Reswap header 'none', got %q", reswap) - } - - // Verify HX-Trigger header set to "refresh-tasks" - trigger := w.Header().Get("HX-Trigger") - if trigger != "refresh-tasks" { - t.Errorf("Expected HX-Trigger header 'refresh-tasks', got %q", trigger) - } - - // Verify response body is empty (no template rendered) - body := w.Body.String() - if body != "" { - t.Errorf("Expected empty response body for uncomplete, got %q", body) - } - - if len(mockTodoist.reopenedTaskIDs) != 1 || mockTodoist.reopenedTaskIDs[0] != "task123" { - t.Errorf("Expected ReopenTask to be called with 'task123', got %v", mockTodoist.reopenedTaskIDs) - } -} - // ============================================================================= // Settings Handler Tests // ============================================================================= @@ -1769,16 +1385,6 @@ func TestHandleGetSourceOptions_MissingSource(t *testing.T) { } } -// mockTodoistClientWithProjects returns mock projects -type mockTodoistClientWithProjects struct { - mockTodoistClient - projects []models.Project -} - -func (m *mockTodoistClientWithProjects) GetProjects(ctx context.Context) ([]models.Project, error) { - return m.projects, nil -} - // mockTrelloClientWithBoards returns mock boards type mockTrelloClientWithBoards struct { mockTrelloClient @@ -1788,14 +1394,6 @@ func TestHandleSyncSources(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() - // Seed tasks table with project info (projects now come from store, not REST API) - tasks := []models.Task{ - {ID: "t1", Content: "Task 1", ProjectID: "proj1", ProjectName: "Project 1", Labels: []string{}}, - {ID: "t2", Content: "Task 2", ProjectID: "proj2", ProjectName: "Project 2", Labels: []string{}}, - } - _ = h.store.SaveTasks(tasks) - - mockTodoist := &mockTodoistClient{} mockTrello := &mockTrelloClientWithBoards{ mockTrelloClient: mockTrelloClient{ boards: []models.Board{ @@ -1804,7 +1402,6 @@ func TestHandleSyncSources(t *testing.T) { }, } - h.todoistClient = mockTodoist h.trelloClient = mockTrello req := httptest.NewRequest("POST", "/settings/sync", nil) @@ -1816,10 +1413,10 @@ func TestHandleSyncSources(t *testing.T) { t.Errorf("Expected status 200, got %d", w.Code) } - // Verify configs were synced from store (not REST API) - configs, _ := h.store.GetSourceConfigsBySource("todoist") - if len(configs) != 2 { - t.Errorf("Expected 2 todoist configs, got %d", len(configs)) + // Verify Trello configs were synced + configs, _ := h.store.GetSourceConfigsBySource("trello") + if len(configs) != 1 { + t.Errorf("Expected 1 trello config, got %d", len(configs)) } } @@ -1921,14 +1518,6 @@ func TestHandleTimeline_RendersDataToTemplate(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() - // Seed tasks with due dates in range - now := config.Now() - todayNoon := time.Date(now.Year(), now.Month(), now.Day(), 12, 0, 0, 0, now.Location()) - tasks := []models.Task{ - {ID: "t1", Content: "Today task", DueDate: &todayNoon, Labels: []string{}, CreatedAt: now}, - } - _ = h.store.SaveTasks(tasks) - req := httptest.NewRequest("GET", "/tabs/timeline", nil) w := httptest.NewRecorder() @@ -1949,14 +1538,10 @@ func TestHandleTimeline_RendersDataToTemplate(t *testing.T) { t.Errorf("Expected template 'timeline-tab', got '%s'", lastCall.Name) } - data, ok := lastCall.Data.(TimelineData) + _, ok := lastCall.Data.(TimelineData) if !ok { t.Fatalf("Expected TimelineData, got %T", lastCall.Data) } - - if len(data.TodayItems) == 0 { - t.Error("Expected TodayItems to contain at least one item, got 0") - } } // assertTemplateContains reads a template file and asserts it contains the expected string. @@ -1979,7 +1564,6 @@ func TestHandleDashboard_ContainsSettingsLink(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() - mockTodoist := &mockTodoistClient{} mockTrello := &mockTrelloClient{} // Use a renderer that outputs actual content so we can check for settings link @@ -1994,11 +1578,10 @@ func TestHandleDashboard_ContainsSettingsLink(t *testing.T) { } h := &Handler{ - store: db, - todoistClient: mockTodoist, - trelloClient: mockTrello, - config: &config.Config{CacheTTLMinutes: 5}, - renderer: renderer, + store: db, + trelloClient: mockTrello, + config: &config.Config{CacheTTLMinutes: 5}, + renderer: renderer, } req := httptest.NewRequest("GET", "/", nil) @@ -2112,17 +1695,15 @@ func TestHandleDashboard_PassesBuildVersion(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() - mockTodoist := &mockTodoistClient{} mockTrello := &mockTrelloClient{} renderer := NewMockRenderer() h := &Handler{ - store: db, - todoistClient: mockTodoist, - trelloClient: mockTrello, - config: &config.Config{CacheTTLMinutes: 5}, - renderer: renderer, - BuildVersion: "abc123", + store: db, + trelloClient: mockTrello, + config: &config.Config{CacheTTLMinutes: 5}, + renderer: renderer, + BuildVersion: "abc123", } req := httptest.NewRequest("GET", "/", nil) @@ -2210,123 +1791,20 @@ func TestHandleTimeline_InvalidParams(t *testing.T) { } } -func TestFetchTasks_FetchesAndSavesTasks(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - h.todoistClient = &mockTodoistClient{ - tasks: []models.Task{ - {ID: "t1", Content: "Task one", Priority: 1}, - {ID: "t2", Content: "Task two", Priority: 2}, - }, - } - - tasks, err := h.fetchTasks(context.Background(), false) - if err != nil { - t.Fatalf("fetchTasks returned error: %v", err) - } - if len(tasks) != 2 { - t.Fatalf("Expected 2 tasks, got %d", len(tasks)) - } - - // Verify tasks are persisted in the store - stored, err := h.store.GetTasks() - if err != nil { - t.Fatalf("Failed to get tasks from store: %v", err) - } - if len(stored) != 2 { - t.Errorf("Expected 2 stored tasks, got %d", len(stored)) - } -} - -func TestFetchTasks_ReturnsCachedTasksWhenValid(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - // Seed cache with tasks and mark it valid - cached := []models.Task{{ID: "cached-1", Content: "Cached task"}} - if err := h.store.SaveTasks(cached); err != nil { - t.Fatalf("Failed to seed tasks: %v", err) - } - if err := h.store.UpdateCacheMetadata(store.CacheKeyTodoistTasks, 60); err != nil { - t.Fatalf("Failed to set cache metadata: %v", err) - } - - // API would return different tasks — should not be called - h.todoistClient = &mockTodoistClient{ - tasks: []models.Task{{ID: "api-1", Content: "API task"}}, - } - - tasks, err := h.fetchTasks(context.Background(), false) - if err != nil { - t.Fatalf("fetchTasks returned error: %v", err) - } - if len(tasks) != 1 || tasks[0].ID != "cached-1" { - t.Errorf("Expected cached task, got %+v", tasks) - } -} - -func TestFetchTasks_ForceRefresh_BypassesCache(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - // Seed cache and mark it valid - if err := h.store.SaveTasks([]models.Task{{ID: "old-1", Content: "Old"}}); err != nil { - t.Fatalf("Failed to seed tasks: %v", err) - } - if err := h.store.UpdateCacheMetadata(store.CacheKeyTodoistTasks, 60); err != nil { - t.Fatalf("Failed to set cache metadata: %v", err) - } - - h.todoistClient = &mockTodoistClient{ - tasks: []models.Task{{ID: "fresh-1", Content: "Fresh"}}, - } - - tasks, err := h.fetchTasks(context.Background(), true) - if err != nil { - t.Fatalf("fetchTasks returned error: %v", err) - } - if len(tasks) != 1 || tasks[0].ID != "fresh-1" { - t.Errorf("Expected fresh task from API, got %+v", tasks) - } -} - -func TestFetchTasks_FallsBackToCacheOnAPIError(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - // Seed stale cache - if err := h.store.SaveTasks([]models.Task{{ID: "stale-1", Content: "Stale"}}); err != nil { - t.Fatalf("Failed to seed tasks: %v", err) - } - - h.todoistClient = &mockTodoistClient{err: fmt.Errorf("API error")} - - tasks, err := h.fetchTasks(context.Background(), false) - if err != nil { - t.Fatalf("Expected fallback to cache, got error: %v", err) - } - if len(tasks) != 1 || tasks[0].ID != "stale-1" { - t.Errorf("Expected stale cached task as fallback, got %+v", tasks) - } -} - // ============================================================================= -// Clear Cache + GetProjects removal tests +// Clear Cache tests // ============================================================================= // TestHandleClearCache verifies that POST /settings/clear-cache invalidates -// all cache keys, clears the todoist sync token, and returns 200 with HX-Trigger. +// all cache keys and returns 200 with HX-Trigger. func TestHandleClearCache(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() - // Seed cache metadata and sync token so we can verify they get cleared - _ = h.store.UpdateCacheMetadata("todoist_tasks", 5) + // Seed cache metadata so we can verify it gets cleared _ = h.store.UpdateCacheMetadata("trello_boards", 5) _ = h.store.UpdateCacheMetadata("plantoeat_meals", 5) _ = h.store.UpdateCacheMetadata("google_calendar", 5) - _ = h.store.SetSyncToken("todoist", "some-sync-token") req := httptest.NewRequest("POST", "/settings/clear-cache", nil) w := httptest.NewRecorder() @@ -2343,62 +1821,20 @@ func TestHandleClearCache(t *testing.T) { } // Verify all cache keys invalidated - for _, key := range []string{"todoist_tasks", "trello_boards", "plantoeat_meals", "google_calendar"} { + for _, key := range []string{"trello_boards", "plantoeat_meals", "google_calendar"} { valid, _ := h.store.IsCacheValid(key) if valid { t.Errorf("Cache key %q should be invalidated but is still valid", key) } } - - // Verify sync token cleared - token, _ := h.store.GetSyncToken("todoist") - if token != "" { - t.Errorf("Expected empty sync token after clear cache, got %q", token) - } } -// TestGetProjectsFromTasks verifies that distinct project pairs are extracted -// from the tasks table. -func TestGetProjectsFromTasks(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - // Seed tasks with different projects (including duplicates and empty) - tasks := []models.Task{ - {ID: "1", Content: "Task 1", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}}, - {ID: "2", Content: "Task 2", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}}, - {ID: "3", Content: "Task 3", ProjectID: "p2", ProjectName: "Work", Labels: []string{}}, - {ID: "4", Content: "Task 4", ProjectID: "", ProjectName: "", Labels: []string{}}, - } - if err := h.store.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to save tasks: %v", err) - } - - projects, err := h.store.GetProjectsFromTasks() - if err != nil { - t.Fatalf("GetProjectsFromTasks returned error: %v", err) - } - - if len(projects) != 2 { - t.Fatalf("Expected 2 distinct projects, got %d: %v", len(projects), projects) - } - - // Verify both projects are present - found := map[string]bool{} - for _, p := range projects { - found[p.ID] = true - } - if !found["p1"] || !found["p2"] { - t.Errorf("Expected projects p1 and p2, got %v", projects) - } -} - -// TestInvalidateAllCaches verifies the convenience method clears all 4 keys. +// TestInvalidateAllCaches verifies the convenience method clears all cache keys. func TestInvalidateAllCaches(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() - keys := []string{"todoist_tasks", "trello_boards", "plantoeat_meals", "google_calendar"} + keys := []string{"trello_boards", "plantoeat_meals", "google_calendar"} for _, key := range keys { _ = h.store.UpdateCacheMetadata(key, 5) } @@ -2423,116 +1859,6 @@ func TestSettingsTemplate_HasClearCacheButton(t *testing.T) { "settings.html must contain a clear-cache button/endpoint") } -// TestAggregateData_DoesNotCallGetProjects verifies that aggregateData no longer -// calls the deprecated GetProjects REST API. -func TestAggregateData_DoesNotCallGetProjects(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - getProjectsCalled := false - mock := &mockTodoistClientTracksGetProjects{ - mockTodoistClient: mockTodoistClient{ - tasks: []models.Task{ - {ID: "1", Content: "Test", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}}, - }, - }, - onGetProjects: func() { getProjectsCalled = true }, - } - h.todoistClient = mock - h.trelloClient = &mockTrelloClient{boards: []models.Board{}} - - _, err := h.aggregateData(context.Background(), false) - if err != nil { - t.Fatalf("aggregateData returned error: %v", err) - } - - if getProjectsCalled { - t.Error("aggregateData should NOT call GetProjects (deprecated REST API)") - } -} - -// TestHandleCreateTask_DoesNotCallGetProjects verifies that HandleCreateTask -// populates projects from the store, not the REST API. -func TestHandleCreateTask_DoesNotCallGetProjects(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - // Seed tasks with project info - tasks := []models.Task{ - {ID: "1", Content: "Existing", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}}, - } - _ = h.store.SaveTasks(tasks) - - getProjectsCalled := false - mock := &mockTodoistClientTracksGetProjects{ - mockTodoistClient: mockTodoistClient{ - tasks: tasks, - }, - onGetProjects: func() { getProjectsCalled = true }, - } - h.todoistClient = mock - - body := strings.NewReader("content=New+Task&project_id=p1") - req := httptest.NewRequest("POST", "/tasks", body) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - w := httptest.NewRecorder() - - h.HandleCreateTask(w, req) - - if getProjectsCalled { - t.Error("HandleCreateTask should NOT call GetProjects (deprecated REST API)") - } -} - -// TestHandleSyncSources_UsesStoreForProjects verifies that HandleSyncSources -// reads projects from the tasks table instead of the REST API. -func TestHandleSyncSources_UsesStoreForProjects(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - // Seed tasks with project info - tasks := []models.Task{ - {ID: "1", Content: "Task1", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}}, - {ID: "2", Content: "Task2", ProjectID: "p2", ProjectName: "Work", Labels: []string{}}, - } - _ = h.store.SaveTasks(tasks) - - getProjectsCalled := false - mock := &mockTodoistClientTracksGetProjects{ - mockTodoistClient: mockTodoistClient{tasks: tasks}, - onGetProjects: func() { getProjectsCalled = true }, - } - h.todoistClient = mock - - req := httptest.NewRequest("POST", "/settings/sync", nil) - w := httptest.NewRecorder() - - h.HandleSyncSources(w, req) - - if getProjectsCalled { - t.Error("HandleSyncSources should NOT call GetProjects (deprecated REST API)") - } - - // Verify todoist project configs were still synced from store - configs, _ := h.store.GetSourceConfigsBySource("todoist") - if len(configs) != 2 { - t.Errorf("Expected 2 todoist configs from store, got %d", len(configs)) - } -} - -// mockTodoistClientTracksGetProjects wraps mockTodoistClient and tracks GetProjects calls. -type mockTodoistClientTracksGetProjects struct { - mockTodoistClient - onGetProjects func() -} - -func (m *mockTodoistClientTracksGetProjects) GetProjects(ctx context.Context) ([]models.Project, error) { - if m.onGetProjects != nil { - m.onGetProjects() - } - return []models.Project{}, nil -} - // --- Sync Log Tests --- // TestStore_AddAndGetSyncLog verifies that sync log entries can be added and retrieved. @@ -2678,7 +2004,7 @@ func TestHandleGetTaskDetail_RendersTemplate(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() - req := httptest.NewRequest("GET", "/tasks/detail?id=abc&source=todoist", nil) + req := httptest.NewRequest("GET", "/tasks/detail?id=abc&source=doot", nil) w := httptest.NewRecorder() h.HandleGetTaskDetail(w, req) @@ -2749,7 +2075,7 @@ func TestHandleSyncSources_AddsLogEntry(t *testing.T) { // HandleTabPlanning data tests // ============================================================================= -// TestHandleTabPlanning_HappyPath verifies that tasks, events, and cards are +// TestHandleTabPlanning_HappyPath verifies that cards are // placed into the correct planning sections (scheduled/unscheduled/upcoming). func TestHandleTabPlanning_HappyPath(t *testing.T) { db, cleanup := setupTestDB(t) @@ -2757,29 +2083,16 @@ func TestHandleTabPlanning_HappyPath(t *testing.T) { today := config.Today() - // Task at 10am today (has time) → goes to Scheduled - taskWithTime := today.Add(10 * time.Hour) - // Task at midnight today (no time component) → goes to Unscheduled - taskNoTime := today - // Task 2 days from now → goes to Upcoming (today+4 is the upper bound) - taskUpcoming := today.AddDate(0, 0, 2) - - tasks := []models.Task{ - {ID: "t-sched", Content: "Scheduled Task", DueDate: &taskWithTime, Labels: []string{}, CreatedAt: time.Now()}, - {ID: "t-unsched", Content: "Unscheduled Task", DueDate: &taskNoTime, Labels: []string{}, CreatedAt: time.Now()}, - {ID: "t-upcoming", Content: "Upcoming Task", DueDate: &taskUpcoming, Labels: []string{}, CreatedAt: time.Now()}, - } - if err := db.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to save tasks: %v", err) - } - // Card at 2pm today → goes to Scheduled cardWithTime := today.Add(14 * time.Hour) + // Card 2 days from now → goes to Upcoming + cardUpcoming := today.AddDate(0, 0, 2) boards := []models.Board{{ ID: "board1", Name: "Board 1", Cards: []models.Card{ {ID: "c-sched", Name: "Scheduled Card", DueDate: &cardWithTime, URL: "http://trello.com/c1"}, + {ID: "c-upcoming", Name: "Upcoming Card", DueDate: &cardUpcoming, URL: "http://trello.com/c2"}, }, }} if err := db.SaveBoards(boards); err != nil { @@ -2818,65 +2131,51 @@ func TestHandleTabPlanning_HappyPath(t *testing.T) { Unscheduled []models.Atom Upcoming []ScheduledItem Boards []models.Board - Projects []models.Project Today string }) if !ok { t.Fatalf("Expected planning data struct, got %T", planningCall.Data) } - // t-sched (10am) and c-sched (2pm) should be in Scheduled - var foundTaskSched, foundCardSched bool + // c-sched (2pm) should be in Scheduled + var foundCardSched bool for _, s := range data.Scheduled { - if s.ID == "t-sched" { - foundTaskSched = true - } if s.ID == "c-sched" { foundCardSched = true } } - if !foundTaskSched { - t.Error("Expected t-sched (task with time today) in Scheduled") - } if !foundCardSched { t.Error("Expected c-sched (card with time today) in Scheduled") } - // t-unsched (midnight, no time) should be in Unscheduled - var foundUnsched bool - for _, u := range data.Unscheduled { - if u.ID == "t-unsched" { - foundUnsched = true - } - } - if !foundUnsched { - t.Error("Expected t-unsched (midnight task) in Unscheduled") - } - - // t-upcoming (2 days out) should be in Upcoming + // c-upcoming (2 days out) should be in Upcoming var foundUpcoming bool for _, u := range data.Upcoming { - if u.ID == "t-upcoming" { + if u.ID == "c-upcoming" { foundUpcoming = true } } if !foundUpcoming { - t.Error("Expected t-upcoming (task in 2 days) in Upcoming") + t.Error("Expected c-upcoming (card in 2 days) in Upcoming") } } -// TestHandleTabPlanning_TomorrowBoundary verifies that a task due exactly at +// TestHandleTabPlanning_TomorrowBoundary verifies that a card due exactly at // midnight of "tomorrow" is NOT before tomorrow, so it lands in Upcoming. func TestHandleTabPlanning_TomorrowBoundary(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() tomorrow := config.Today().AddDate(0, 0, 1) // midnight tomorrow - tasks := []models.Task{ - {ID: "t-boundary", Content: "Midnight Tomorrow", DueDate: &tomorrow, Labels: []string{}, CreatedAt: time.Now()}, - } - if err := db.SaveTasks(tasks); err != nil { - t.Fatalf("Failed to save tasks: %v", err) + boards := []models.Board{{ + ID: "board1", + Name: "Board 1", + Cards: []models.Card{ + {ID: "c-boundary", Name: "Midnight Tomorrow", DueDate: &tomorrow, URL: "http://trello.com/c1"}, + }, + }} + if err := db.SaveBoards(boards); err != nil { + t.Fatalf("Failed to save boards: %v", err) } renderer := NewMockRenderer() @@ -2911,7 +2210,6 @@ func TestHandleTabPlanning_TomorrowBoundary(t *testing.T) { Unscheduled []models.Atom Upcoming []ScheduledItem Boards []models.Board - Projects []models.Project Today string }) if !ok { @@ -2920,24 +2218,24 @@ func TestHandleTabPlanning_TomorrowBoundary(t *testing.T) { // midnight-tomorrow: !dueDate.Before(tomorrow) → not in scheduled/unscheduled for _, s := range data.Scheduled { - if s.ID == "t-boundary" { - t.Error("Midnight-tomorrow task must not appear in Scheduled") + if s.ID == "c-boundary" { + t.Error("Midnight-tomorrow card must not appear in Scheduled") } } for _, u := range data.Unscheduled { - if u.ID == "t-boundary" { - t.Error("Midnight-tomorrow task must not appear in Unscheduled") + if u.ID == "c-boundary" { + t.Error("Midnight-tomorrow card must not appear in Unscheduled") } } // dueDate.Before(in3Days=today+4) → lands in Upcoming var foundUpcoming bool for _, u := range data.Upcoming { - if u.ID == "t-boundary" { + if u.ID == "c-boundary" { foundUpcoming = true } } if !foundUpcoming { - t.Error("Expected midnight-tomorrow task in Upcoming") + t.Error("Expected midnight-tomorrow card in Upcoming") } } diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go index 32e0215..dc345db 100644 --- a/internal/handlers/settings.go +++ b/internal/handlers/settings.go @@ -34,7 +34,7 @@ func (h *Handler) HandleSettingsPage(w http.ResponseWriter, r *http.Request) { WebAuthnEnabled bool }{ Configs: bySource, - Sources: []string{"trello", "todoist", "gcal", "gtasks"}, + Sources: []string{"trello", "gcal", "gtasks"}, Toggles: toggles, SyncLog: syncLog, Agents: agents, @@ -68,20 +68,6 @@ func (h *Handler) HandleSyncSources(w http.ResponseWriter, r *http.Request) { } } - // Sync Todoist projects from cached tasks (avoids deprecated REST API) - if projects, err := h.store.GetProjectsFromTasks(); err == nil && len(projects) > 0 { - var items []models.SourceConfig - for _, p := range projects { - items = append(items, models.SourceConfig{ - Source: "todoist", - ItemType: "project", - ItemID: p.ID, - ItemName: p.Name, - }) - } - _ = h.store.SyncSourceConfigs("todoist", "project", items) - } - // Sync Google Calendar calendars if h.googleCalendarClient != nil { calendars, err := h.googleCalendarClient.GetCalendarList(ctx) @@ -122,18 +108,13 @@ func (h *Handler) HandleSyncSources(w http.ResponseWriter, r *http.Request) { h.HandleSettingsPage(w, r) } -// HandleClearCache invalidates all caches and clears the Todoist sync token +// HandleClearCache invalidates all caches func (h *Handler) HandleClearCache(w http.ResponseWriter, r *http.Request) { if err := h.store.InvalidateAllCaches(); err != nil { JSONError(w, http.StatusInternalServerError, "Failed to clear cache", err) return } - if err := h.store.ClearSyncToken("todoist"); err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to clear sync token", err) - return - } - _ = h.store.AddSyncLogEntry("cache_clear", "Cache cleared — next load fetches fresh data") syncLog, _ := h.store.GetRecentSyncLog(20) @@ -167,8 +148,6 @@ func (h *Handler) HandleToggleSourceConfig(w http.ResponseWriter, r *http.Reques switch source { case "trello": _ = h.store.InvalidateCache("trello_boards") - case "todoist": - _ = h.store.InvalidateCache("todoist_tasks") } w.Header().Set("Content-Type", "application/json") diff --git a/internal/handlers/tab_state_test.go b/internal/handlers/tab_state_test.go index d066966..9b34033 100644 --- a/internal/handlers/tab_state_test.go +++ b/internal/handlers/tab_state_test.go @@ -33,10 +33,9 @@ func TestHandleDashboard_TabState(t *testing.T) { } h := &Handler{ - store: db, - renderer: mock, - todoistClient: &mockTodoistClient{}, - trelloClient: &mockTrelloClient{}, + store: db, + renderer: mock, + trelloClient: &mockTrelloClient{}, config: &config.Config{ Port: "8080", CacheTTLMinutes: 5, diff --git a/internal/handlers/timeline_logic.go b/internal/handlers/timeline_logic.go index c9510bf..67ba09d 100644 --- a/internal/handlers/timeline_logic.go +++ b/internal/handlers/timeline_logic.go @@ -17,54 +17,7 @@ func BuildTimeline(ctx context.Context, s *store.Store, start, end time.Time) ([ var items []models.TimelineItem now := config.Now() - // 1. Fetch Tasks (dated) - tasks, err := s.GetTasksByDateRange(start, end) - if err != nil { - return nil, err - } - for _, task := range tasks { - if task.DueDate == nil { - continue - } - item := models.TimelineItem{ - ID: task.ID, - Type: models.TimelineItemTypeTask, - Title: task.Content, - Time: *task.DueDate, - Description: task.Description, - URL: task.URL, - OriginalItem: task, - IsCompleted: task.Completed, - Source: "todoist", - } - item.ComputeDaySection(now) - items = append(items, item) - } - - // 1b. Fetch undated Tasks — place in Today section - undatedTasks, err := s.GetUndatedTasks() - if err != nil { - log.Printf("Warning: failed to fetch undated tasks: %v", err) - } else { - for _, task := range undatedTasks { - item := models.TimelineItem{ - ID: task.ID, - Type: models.TimelineItemTypeTask, - Title: task.Content, - Time: now, - Description: task.Description, - URL: task.URL, - OriginalItem: task, - IsCompleted: task.Completed, - Source: "todoist", - IsAllDay: true, - } - item.ComputeDaySection(now) - items = append(items, item) - } - } - - // 2. Fetch Meals - combine multiple items for same date+mealType + // 1. Fetch Meals - combine multiple items for same date+mealType meals, err := s.GetMealsByDateRange(start, end) if err != nil { return nil, err diff --git a/internal/handlers/timeline_logic_test.go b/internal/handlers/timeline_logic_test.go index 03bc291..3678b62 100644 --- a/internal/handlers/timeline_logic_test.go +++ b/internal/handlers/timeline_logic_test.go @@ -46,20 +46,6 @@ func setupTestStore(t *testing.T) *store.Store { } schema := ` - CREATE TABLE IF NOT EXISTS tasks ( - id TEXT PRIMARY KEY, - content TEXT NOT NULL, - description TEXT, - project_id TEXT, - project_name TEXT, - due_date DATETIME, - priority INTEGER DEFAULT 1, - completed BOOLEAN DEFAULT FALSE, - labels TEXT, - url TEXT, - created_at DATETIME, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP - ); CREATE TABLE IF NOT EXISTS meals ( id TEXT PRIMARY KEY, recipe_name TEXT NOT NULL, @@ -118,15 +104,9 @@ func setupTestStore(t *testing.T) *store.Store { func TestBuildTimeline(t *testing.T) { s := setupTestStore(t) - + // Fix a base time: 2023-01-01 08:00:00 baseTime := time.Date(2023, 1, 1, 8, 0, 0, 0, time.UTC) - - // Task: 10:00 - taskDate := baseTime.Add(2 * time.Hour) - _ = s.SaveTasks([]models.Task{ - {ID: "t1", Content: "Task 1", DueDate: &taskDate}, - }) // Meal: Lunch (defaults to 12:00) mealDate := baseTime // Date part matters @@ -138,7 +118,7 @@ func TestBuildTimeline(t *testing.T) { cardDate := baseTime.Add(6 * time.Hour) _ = s.SaveBoards([]models.Board{ { - ID: "b1", + ID: "b1", Name: "Board 1", Cards: []models.Card{ {ID: "c1", Name: "Card 1", DueDate: &cardDate, ListID: "l1"}, @@ -161,27 +141,23 @@ func TestBuildTimeline(t *testing.T) { t.Fatalf("BuildTimeline failed: %v", err) } - if len(items) != 4 { - t.Errorf("Expected 4 items, got %d", len(items)) + if len(items) != 3 { + t.Errorf("Expected 3 items, got %d", len(items)) } // Expected Order: // 1. Event (09:00) - // 2. Task (10:00) - // 3. Meal (12:00) - // 4. Card (14:00) + // 2. Meal (12:00) + // 3. Card (14:00) if items[0].Type != models.TimelineItemTypeEvent { t.Errorf("Expected item 0 to be Event, got %s", items[0].Type) } - if items[1].Type != models.TimelineItemTypeTask { - t.Errorf("Expected item 1 to be Task, got %s", items[1].Type) + if items[1].Type != models.TimelineItemTypeMeal { + t.Errorf("Expected item 1 to be Meal, got %s", items[1].Type) } - if items[2].Type != models.TimelineItemTypeMeal { - t.Errorf("Expected item 2 to be Meal, got %s", items[2].Type) - } - if items[3].Type != models.TimelineItemTypeCard { - t.Errorf("Expected item 3 to be Card, got %s", items[3].Type) + if items[2].Type != models.TimelineItemTypeCard { + t.Errorf("Expected item 2 to be Card, got %s", items[2].Type) } } @@ -273,78 +249,6 @@ func TestCalcCalendarBounds(t *testing.T) { } } -func TestBuildTimeline_IncludesOverdueItems(t *testing.T) { - s := setupTestStore(t) - - // Base: "today" is Jan 2, 2023 - today := time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC) - yesterday := time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC) // overdue - todayNoon := time.Date(2023, 1, 2, 12, 0, 0, 0, time.UTC) // current - - // Save a task that is overdue (yesterday) and one that is current (today) - _ = s.SaveTasks([]models.Task{ - {ID: "overdue1", Content: "Overdue task", DueDate: &yesterday}, - {ID: "current1", Content: "Current task", DueDate: &todayNoon}, - }) - - // Query range: today through tomorrow - end := today.AddDate(0, 0, 1) - - items, err := BuildTimeline(context.Background(), s, today, end) - if err != nil { - t.Fatalf("BuildTimeline failed: %v", err) - } - - // Should include both the overdue task and the current task - if len(items) < 2 { - t.Errorf("Expected at least 2 items (overdue + current), got %d", len(items)) - for _, item := range items { - t.Logf(" item: %s (type=%s, time=%s)", item.Title, item.Type, item.Time) - } - } - - // Verify overdue task is marked as overdue - foundOverdue := false - for _, item := range items { - if item.ID == "overdue1" { - foundOverdue = true - if !item.IsOverdue { - t.Error("Expected overdue task to be marked IsOverdue=true") - } - if item.DaySection != models.DaySectionToday { - t.Errorf("Expected overdue task in Today section, got %s", item.DaySection) - } - } - } - if !foundOverdue { - t.Error("Overdue task was not included in timeline results") - } -} - -func TestBuildTimeline_ExcludesCompletedOverdue(t *testing.T) { - s := setupTestStore(t) - - yesterday := time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC) - today := time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC) - end := today.AddDate(0, 0, 1) - - // Save a completed overdue task — should NOT appear - _ = s.SaveTasks([]models.Task{ - {ID: "done1", Content: "Done overdue", DueDate: &yesterday, Completed: true}, - }) - - items, err := BuildTimeline(context.Background(), s, today, end) - if err != nil { - t.Fatalf("BuildTimeline failed: %v", err) - } - - for _, item := range items { - if item.ID == "done1" { - t.Error("Completed overdue task should not appear in timeline") - } - } -} - func TestBuildTimeline_ReadsCalendarEventsFromStore(t *testing.T) { s := setupTestStore(t) @@ -483,36 +387,6 @@ func TestSaveAndGetCalendarEvents(t *testing.T) { } } -func TestBuildTimeline_IncludesUndatedTodoistTasks(t *testing.T) { - s := setupTestStore(t) - - // Save a task with no due date - _ = s.SaveTasks([]models.Task{ - {ID: "undated1", Content: "Take out recycling"}, // DueDate is nil - }) - - start := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) - end := time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC) - - items, err := BuildTimeline(context.Background(), s, start, end) - if err != nil { - t.Fatalf("BuildTimeline failed: %v", err) - } - - found := false - for _, item := range items { - if item.ID == "undated1" { - found = true - if item.DaySection != models.DaySectionToday { - t.Errorf("Undated task should be in Today section, got %s", item.DaySection) - } - } - } - if !found { - t.Error("Undated Todoist task should appear in timeline Today section") - } -} - func timePtr(t time.Time) *time.Time { return &t } diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go index ee13dc4..876aaad 100644 --- a/internal/handlers/widget.go +++ b/internal/handlers/widget.go @@ -47,7 +47,7 @@ func TimelineItemToWidgetItem(item models.TimelineItem) models.WidgetItem { // not completable via widget API default: wi.Type = "task" - wi.Completable = item.Source == "todoist" || item.Source == "doot" + wi.Completable = item.Source == "doot" } // Only populate Start/End for items with a real time (non-all-day, non-zero) @@ -147,15 +147,6 @@ func (h *Handler) HandleWidgetComplete(w http.ResponseWriter, r *http.Request) { return } _ = h.store.SaveCompletedTask("doot", req.ID, "", nil) - case "todoist": - if h.todoistClient == nil { - http.Error(w, "todoist not configured", http.StatusServiceUnavailable) - return - } - if err := h.todoistClient.CompleteTask(r.Context(), req.ID); err != nil { - http.Error(w, "failed to complete task", http.StatusInternalServerError) - return - } default: http.Error(w, "source not completable", http.StatusBadRequest) return diff --git a/internal/handlers/widget_test.go b/internal/handlers/widget_test.go index 5b054a0..f56fe07 100644 --- a/internal/handlers/widget_test.go +++ b/internal/handlers/widget_test.go @@ -61,11 +61,11 @@ func TestTimelineItemToWidgetItem_Task(t *testing.T) { item := models.TimelineItem{ ID: "abc", Title: "Write notes", - Source: "todoist", + Source: "doot", Type: models.TimelineItemTypeTask, Time: now, IsAllDay: true, - URL: "https://todoist.com/task/abc", + URL: "https://example.com/task/abc", } wi := TimelineItemToWidgetItem(item) @@ -77,7 +77,7 @@ func TestTimelineItemToWidgetItem_Task(t *testing.T) { t.Errorf("Type: got %q, want %q", wi.Type, "task") } if !wi.Completable { - t.Error("todoist task should be completable") + t.Error("doot task should be completable") } if wi.Start != nil { t.Error("all-day item should have nil Start") @@ -109,7 +109,7 @@ func TestTimelineItemToWidgetItem_Event(t *testing.T) { } } -func TestHandleWidgetComplete_NonTodoist(t *testing.T) { +func TestHandleWidgetComplete_NonCompletable(t *testing.T) { h := &Handler{} body := `{"id":"x","source":"calendar"}` req := httptest.NewRequest("POST", "/api/widget/complete", strings.NewReader(body)) -- cgit v1.2.3