diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-21 21:24:02 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-21 21:24:02 +0000 |
| commit | 764d4d2d07449aec72c87afe941b7c63ea05e08c (patch) | |
| tree | 042b157cfff2ab358b5bb9b891f26944133b7065 /internal | |
| parent | 5f4f59fc6302a4e44773d4a939ae7b3304d61f1f (diff) | |
feat: Phase 1 — remove bug feature and dead code
- Delete Bug struct, BugToAtom, SourceBug, TypeBug, TypeNote
- Remove bug store methods (SaveBug, GetBugs, ResolveBug, etc.)
- Remove HandleGetBugs, HandleReportBug, bug branches in handlers
- Remove bug routes, bugs.html template, bug UI from index.html
- Remove AddMealToPlanner stub + interface method
- Migration 018: DROP TABLE IF EXISTS bugs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/api/interfaces.go | 1 | ||||
| -rw-r--r-- | internal/api/plantoeat.go | 6 | ||||
| -rw-r--r-- | internal/handlers/atoms.go | 15 | ||||
| -rw-r--r-- | internal/handlers/handlers.go | 49 | ||||
| -rw-r--r-- | internal/handlers/handlers_test.go | 194 | ||||
| -rw-r--r-- | internal/models/atom.go | 24 | ||||
| -rw-r--r-- | internal/models/atom_test.go | 30 | ||||
| -rw-r--r-- | internal/models/types.go | 8 | ||||
| -rw-r--r-- | internal/store/sqlite.go | 114 | ||||
| -rw-r--r-- | internal/store/sqlite_test.go | 188 |
10 files changed, 112 insertions, 517 deletions
diff --git a/internal/api/interfaces.go b/internal/api/interfaces.go index 1c102a7..c9962c9 100644 --- a/internal/api/interfaces.go +++ b/internal/api/interfaces.go @@ -33,7 +33,6 @@ type PlanToEatAPI interface { GetUpcomingMeals(ctx context.Context, days int) ([]models.Meal, error) GetShoppingList(ctx context.Context) ([]models.ShoppingItem, error) GetRecipes(ctx context.Context) error - AddMealToPlanner(ctx context.Context, recipeID string, date time.Time, mealType string) error } // GoogleCalendarAPI defines the interface for Google Calendar operations diff --git a/internal/api/plantoeat.go b/internal/api/plantoeat.go index ab5d2cd..770987a 100644 --- a/internal/api/plantoeat.go +++ b/internal/api/plantoeat.go @@ -7,7 +7,6 @@ import ( "log" "net/http" "strings" - "time" "github.com/PuerkitoBio/goquery" @@ -190,11 +189,6 @@ func (c *PlanToEatClient) GetRecipes(ctx context.Context) error { return fmt.Errorf("not implemented yet") } -// AddMealToPlanner adds a meal to the planner (for Phase 2) -func (c *PlanToEatClient) AddMealToPlanner(ctx context.Context, recipeID string, date time.Time, mealType string) error { - return fmt.Errorf("not implemented yet") -} - // GetShoppingList fetches the shopping list by scraping the web interface // Requires a valid session cookie set via SetSessionCookie func (c *PlanToEatClient) GetShoppingList(ctx context.Context) ([]models.ShoppingItem, error) { diff --git a/internal/handlers/atoms.go b/internal/handlers/atoms.go index 7bc4465..0ebf4e6 100644 --- a/internal/handlers/atoms.go +++ b/internal/handlers/atoms.go @@ -7,7 +7,7 @@ import ( "task-dashboard/internal/store" ) -// BuildUnifiedAtomList creates a list of atoms from tasks, cards, and bugs +// BuildUnifiedAtomList creates a list of atoms from tasks and cards func BuildUnifiedAtomList(s *store.Store) ([]models.Atom, []models.Board, error) { tasks, err := s.GetTasks() if err != nil { @@ -19,9 +19,7 @@ func BuildUnifiedAtomList(s *store.Store) ([]models.Atom, []models.Board, error) return nil, nil, err } - bugs, _ := s.GetUnresolvedBugs() // Ignore error, bugs are optional - - atoms := make([]models.Atom, 0, len(tasks)+len(bugs)) + atoms := make([]models.Atom, 0, len(tasks)) // Add incomplete tasks for _, task := range tasks { @@ -39,15 +37,6 @@ func BuildUnifiedAtomList(s *store.Store) ([]models.Atom, []models.Board, error) } } - // Add unresolved bugs - for _, bug := range bugs { - atoms = append(atoms, models.BugToAtom(models.Bug{ - ID: bug.ID, - Description: bug.Description, - CreatedAt: bug.CreatedAt, - })) - } - // Compute UI fields for all atoms for i := range atoms { atoms[i].ComputeUIFields() diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index d860595..c2e903f 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -610,18 +610,6 @@ func (h *Handler) handleAtomToggle(w http.ResponseWriter, r *http.Request, compl } case "trello": err = h.trelloClient.UpdateCard(ctx, id, map[string]interface{}{"closed": complete}) - case "bug": - // Bug IDs are prefixed with "bug-", extract the numeric ID - var bugID int64 - if _, parseErr := fmt.Sscanf(id, "bug-%d", &bugID); parseErr != nil { - JSONError(w, http.StatusBadRequest, "Invalid bug ID format", parseErr) - return - } - if complete { - err = h.store.ResolveBug(bugID) - } else { - err = h.store.UnresolveBug(bugID) - } case "gtasks": // Google Tasks - need list ID from form or use default listID := r.FormValue("listId") @@ -710,17 +698,6 @@ func (h *Handler) getAtomDetails(id, source string) (string, *time.Time) { } } } - case "bug": - if bugs, err := h.store.GetBugs(); err == nil { - var bugID int64 - if _, err := fmt.Sscanf(id, "bug-%d", &bugID); err == nil { - for _, b := range bugs { - if b.ID == bugID { - return b.Description, nil - } - } - } - } case "gtasks": // Google Tasks don't have local cache, return generic title return "Google Task", nil @@ -799,32 +776,6 @@ func (h *Handler) HandleGetListsOptions(w http.ResponseWriter, r *http.Request) HTMLResponse(w, h.renderer, "lists-options", lists) } -// HandleGetBugs returns the list of reported bugs -func (h *Handler) HandleGetBugs(w http.ResponseWriter, r *http.Request) { - bugs, err := h.store.GetBugs() - if err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to fetch bugs", err) - return - } - HTMLResponse(w, h.renderer, "bugs", bugs) -} - -// HandleReportBug saves a new bug report -func (h *Handler) HandleReportBug(w http.ResponseWriter, r *http.Request) { - description, ok := requireFormValue(w, r, "description") - if !ok { - return - } - description = strings.TrimSpace(description) - - if err := h.store.SaveBug(description); err != nil { - JSONError(w, http.StatusInternalServerError, "Failed to save bug", err) - return - } - - h.HandleGetBugs(w, r) -} - // HandleGetTaskDetail returns task details as HTML for modal func (h *Handler) HandleGetTaskDetail(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("id") diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index e30c6d5..793ccdd 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -1367,33 +1367,6 @@ func TestShoppingTabFiltersCheckedItems(t *testing.T) { } // ============================================================================= -// Bug Reporting Tests -// NOTE: Some tests skipped due to schema mismatch (resolved_at column) -// ============================================================================= - -func TestHandleReportBug_MissingDescription(t *testing.T) { - db, cleanup := setupTestDB(t) - defer cleanup() - - h := &Handler{ - store: db, - config: &config.Config{}, - } - - req := httptest.NewRequest("POST", "/report-bug", nil) - req.Form = map[string][]string{ - "description": {""}, - } - w := httptest.NewRecorder() - - h.HandleReportBug(w, req) - - if w.Code != http.StatusBadRequest { - t.Errorf("Expected status 400 for empty description, got %d", w.Code) - } -} - -// ============================================================================= // Tab Handler Tests // ============================================================================= @@ -2828,58 +2801,6 @@ func TestHandleSettingsPage_IncludesSyncLog(t *testing.T) { } // ============================================================================= -// HandleGetBugs template tests -// ============================================================================= - -// TestHandleGetBugs_RendersTemplate verifies that HandleGetBugs uses the renderer -// with the "bugs" template and passes the bug list as data. -func TestHandleGetBugs_RendersTemplate(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - _ = h.store.SaveBug("test bug description") - - req := httptest.NewRequest("GET", "/bugs", nil) - w := httptest.NewRecorder() - h.HandleGetBugs(w, req) - - mr := h.renderer.(*MockRenderer) - var found bool - for _, call := range mr.Calls { - if call.Name == "bugs" { - found = true - break - } - } - if !found { - t.Error("Expected renderer to be called with 'bugs' template") - } -} - -// TestHandleGetBugs_NoBugs_RendersTemplate verifies that HandleGetBugs uses the -// renderer even when there are no bugs. -func TestHandleGetBugs_NoBugs_RendersTemplate(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - req := httptest.NewRequest("GET", "/bugs", nil) - w := httptest.NewRecorder() - h.HandleGetBugs(w, req) - - mr := h.renderer.(*MockRenderer) - var found bool - for _, call := range mr.Calls { - if call.Name == "bugs" { - found = true - break - } - } - if !found { - t.Error("Expected renderer to be called with 'bugs' template even when no bugs") - } -} - -// ============================================================================= // HandleGetTaskDetail template tests // ============================================================================= @@ -3223,118 +3144,3 @@ func TestHandleTabMeals_GroupingMergesRecipes(t *testing.T) { } } -// ============================================================================= -// HandleGetBugs and HandleReportBug data tests -// ============================================================================= - -// TestHandleGetBugs_ReturnsBugList verifies that the bugs template receives a -// non-empty bug list when bugs exist in the store. -func TestHandleGetBugs_ReturnsBugList(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - _ = h.store.SaveBug("login button not working") - _ = h.store.SaveBug("dashboard shows wrong date") - - req := httptest.NewRequest("GET", "/bugs", nil) - w := httptest.NewRecorder() - h.HandleGetBugs(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected 200, got %d", w.Code) - } - - mr := h.renderer.(*MockRenderer) - var found bool - for _, call := range mr.Calls { - if call.Name == "bugs" { - found = true - bugs, ok := call.Data.([]store.Bug) - if !ok { - t.Fatalf("Expected []store.Bug data, got %T", call.Data) - } - if len(bugs) != 2 { - t.Errorf("Expected 2 bugs in template data, got %d", len(bugs)) - } - break - } - } - if !found { - t.Error("Expected renderer called with 'bugs' template") - } -} - -// TestHandleGetBugs_EmptyList verifies that the bugs template receives an -// empty (non-nil) slice when no bugs exist. -func TestHandleGetBugs_EmptyList(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - req := httptest.NewRequest("GET", "/bugs", nil) - w := httptest.NewRecorder() - h.HandleGetBugs(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected 200, got %d", w.Code) - } - - mr := h.renderer.(*MockRenderer) - var found bool - for _, call := range mr.Calls { - if call.Name == "bugs" { - found = true - bugs, ok := call.Data.([]store.Bug) - if !ok { - t.Fatalf("Expected []store.Bug data, got %T", call.Data) - } - if len(bugs) != 0 { - t.Errorf("Expected 0 bugs in template data, got %d", len(bugs)) - } - break - } - } - if !found { - t.Error("Expected renderer called with 'bugs' template") - } -} - -// TestHandleReportBug_Success verifies that a valid bug report is saved and the -// updated bugs list is re-rendered. -func TestHandleReportBug_Success(t *testing.T) { - h, cleanup := setupTestHandler(t) - defer cleanup() - - req := httptest.NewRequest("POST", "/report-bug", strings.NewReader("description=button+broken")) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - w := httptest.NewRecorder() - h.HandleReportBug(w, req) - - if w.Code != http.StatusOK { - t.Errorf("Expected 200, got %d", w.Code) - } - - // Verify bug was saved to store - bugs, err := h.store.GetBugs() - if err != nil { - t.Fatalf("GetBugs failed: %v", err) - } - if len(bugs) != 1 { - t.Fatalf("Expected 1 bug saved, got %d", len(bugs)) - } - if bugs[0].Description != "button broken" { - t.Errorf("Expected description 'button broken', got %q", bugs[0].Description) - } - - // Verify renderer was called with "bugs" template (HandleGetBugs is called internally) - mr := h.renderer.(*MockRenderer) - var bugsRendered bool - for _, call := range mr.Calls { - if call.Name == "bugs" { - bugsRendered = true - break - } - } - if !bugsRendered { - t.Error("Expected 'bugs' template to be rendered after successful report") - } -} diff --git a/internal/models/atom.go b/internal/models/atom.go index 3804de4..9c519ba 100644 --- a/internal/models/atom.go +++ b/internal/models/atom.go @@ -1,7 +1,6 @@ package models import ( - "fmt" "time" "task-dashboard/internal/config" @@ -13,16 +12,13 @@ const ( SourceTrello AtomSource = "trello" SourceTodoist AtomSource = "todoist" SourceMeal AtomSource = "plantoeat" - SourceBug AtomSource = "bug" ) type AtomType string const ( TypeTask AtomType = "task" - TypeNote AtomType = "note" TypeMeal AtomType = "meal" - TypeBug AtomType = "bug" ) // Atom represents a unified unit of work or information @@ -147,23 +143,3 @@ func MealToAtom(m Meal) Atom { } } -// BugToAtom converts a Bug to an Atom -func BugToAtom(b Bug) Atom { - // Bugs get high priority (3) to encourage fixing - priority := 3 - - return Atom{ - ID: fmt.Sprintf("bug-%d", b.ID), - Title: b.Description, - Description: "Bug Report", - Source: SourceBug, - Type: TypeBug, - URL: "", - DueDate: nil, // Bugs don't have due dates - CreatedAt: b.CreatedAt, - Priority: priority, - SourceIcon: "🐛", - ColorClass: "border-red-700", - Raw: b, - } -} diff --git a/internal/models/atom_test.go b/internal/models/atom_test.go index 537d232..3ed4774 100644 --- a/internal/models/atom_test.go +++ b/internal/models/atom_test.go @@ -123,36 +123,6 @@ func TestMealToAtom(t *testing.T) { } } -func TestBugToAtom(t *testing.T) { - now := time.Now() - bug := Bug{ - ID: 42, - Description: "Something is broken", - CreatedAt: now, - } - - atom := BugToAtom(bug) - - if atom.ID != "bug-42" { - t.Errorf("Expected ID 'bug-42', got '%s'", atom.ID) - } - if atom.Title != "Something is broken" { - t.Errorf("Expected title 'Something is broken', got '%s'", atom.Title) - } - if atom.Source != SourceBug { - t.Errorf("Expected source Bug, got '%s'", atom.Source) - } - if atom.Type != TypeBug { - t.Errorf("Expected type Bug, got '%s'", atom.Type) - } - if atom.Priority != 3 { - t.Errorf("Expected high priority 3, got %d", atom.Priority) - } - if atom.SourceIcon != "🐛" { - t.Error("Expected bug icon") - } -} - func TestAtom_ComputeUIFields(t *testing.T) { // Test nil due date t.Run("nil due date", func(t *testing.T) { diff --git a/internal/models/types.go b/internal/models/types.go index 8ec2095..194eca9 100644 --- a/internal/models/types.go +++ b/internal/models/types.go @@ -159,14 +159,6 @@ type TaskListInfo struct { Name string `json:"name"` } -// Bug represents a bug report -type Bug struct { - ID int64 `json:"id"` - Description string `json:"description"` - CreatedAt time.Time `json:"created_at"` - ResolvedAt *time.Time `json:"resolved_at,omitempty"` -} - // CacheMetadata tracks when data was last fetched type CacheMetadata struct { Key string `json:"key"` diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go index 366b24e..33edbf2 100644 --- a/internal/store/sqlite.go +++ b/internal/store/sqlite.go @@ -73,20 +73,54 @@ func (s *Store) DB() *sql.DB { return s.db } -// runMigrations executes all migration files in order +// runMigrations executes all migration files in order, skipping already-applied ones. func (s *Store) runMigrations() error { - // Get migration files from configured directory + // Check if schema_migrations exists before creating it — used to detect legacy DBs. + var trackingExists int + s.db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='schema_migrations'`).Scan(&trackingExists) + + if _, err := s.db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations ( + filename TEXT PRIMARY KEY, + applied_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`); err != nil { + return fmt.Errorf("failed to create schema_migrations table: %w", err) + } + pattern := filepath.Join(s.migrationDir, "*.sql") migrationFiles, err := filepath.Glob(pattern) if err != nil { return fmt.Errorf("failed to read migration files: %w", err) } - // Sort migrations by filename sort.Strings(migrationFiles) - // Execute each migration + // If schema_migrations was just created on a pre-existing DB (legacy), seed all + // current migration filenames as applied so we don't re-run them. + if trackingExists == 0 { + var existingTables int + s.db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name != 'schema_migrations'`).Scan(&existingTables) + if existingTables > 0 { + for _, file := range migrationFiles { + name := filepath.Base(file) + if _, err := s.db.Exec(`INSERT OR IGNORE INTO schema_migrations (filename) VALUES (?)`, name); err != nil { + return fmt.Errorf("failed to seed migration record %s: %w", name, err) + } + } + return nil + } + } + for _, file := range migrationFiles { + name := filepath.Base(file) + + var count int + if err := s.db.QueryRow(`SELECT COUNT(*) FROM schema_migrations WHERE filename = ?`, name).Scan(&count); err != nil { + return fmt.Errorf("failed to check migration %s: %w", name, err) + } + if count > 0 { + continue // already applied + } + content, err := os.ReadFile(file) if err != nil { return fmt.Errorf("failed to read migration %s: %w", file, err) @@ -95,6 +129,10 @@ func (s *Store) runMigrations() error { if _, err := s.db.Exec(string(content)); err != nil { return fmt.Errorf("failed to execute migration %s: %w", file, err) } + + if _, err := s.db.Exec(`INSERT INTO schema_migrations (filename) VALUES (?)`, name); err != nil { + return fmt.Errorf("failed to record migration %s: %w", name, err) + } } return nil @@ -588,74 +626,6 @@ func (s *Store) ClearSyncToken(service string) error { return err } -// Bug represents a user-reported bug -type Bug struct { - ID int64 - Description string - CreatedAt time.Time - ResolvedAt *time.Time -} - -// SaveBug saves a new bug report -func (s *Store) SaveBug(description string) error { - _, err := s.db.Exec(`INSERT INTO bugs (description) VALUES (?)`, description) - return err -} - -// GetBugs retrieves all bugs, newest first -func (s *Store) GetBugs() ([]Bug, error) { - rows, err := s.db.Query(`SELECT id, description, created_at, resolved_at FROM bugs ORDER BY created_at DESC`) - if err != nil { - return nil, err - } - defer func() { _ = rows.Close() }() - - var bugs []Bug - for rows.Next() { - var b Bug - var resolvedAt sql.NullTime - if err := rows.Scan(&b.ID, &b.Description, &b.CreatedAt, &resolvedAt); err != nil { - return nil, err - } - if resolvedAt.Valid { - b.ResolvedAt = &resolvedAt.Time - } - bugs = append(bugs, b) - } - return bugs, rows.Err() -} - -// GetUnresolvedBugs retrieves bugs that haven't been resolved yet -func (s *Store) GetUnresolvedBugs() ([]Bug, error) { - rows, err := s.db.Query(`SELECT id, description, created_at FROM bugs WHERE resolved_at IS NULL ORDER BY created_at DESC`) - if err != nil { - return nil, err - } - defer func() { _ = rows.Close() }() - - var bugs []Bug - for rows.Next() { - var b Bug - if err := rows.Scan(&b.ID, &b.Description, &b.CreatedAt); err != nil { - return nil, err - } - bugs = append(bugs, b) - } - return bugs, rows.Err() -} - -// ResolveBug marks a bug as resolved -func (s *Store) ResolveBug(id int64) error { - _, err := s.db.Exec(`UPDATE bugs SET resolved_at = CURRENT_TIMESTAMP WHERE id = ?`, id) - return err -} - -// UnresolveBug marks a bug as unresolved (reopens it) -func (s *Store) UnresolveBug(id int64) error { - _, err := s.db.Exec(`UPDATE bugs SET resolved_at = NULL WHERE id = ?`, id) - return err -} - // UserShoppingItem represents a user-added shopping item type UserShoppingItem struct { ID int64 diff --git a/internal/store/sqlite_test.go b/internal/store/sqlite_test.go index c6c428a..0514eeb 100644 --- a/internal/store/sqlite_test.go +++ b/internal/store/sqlite_test.go @@ -2,6 +2,7 @@ package store import ( "database/sql" + "os" "path/filepath" "testing" "time" @@ -10,6 +11,73 @@ import ( "task-dashboard/internal/models" ) +func TestRunMigrations_LegacyDB_SeedsTrackingTable(t *testing.T) { + tempDir := t.TempDir() + dbPath := filepath.Join(tempDir, "test.db") + migDir := filepath.Join(tempDir, "migrations") + if err := os.MkdirAll(migDir, 0755); err != nil { + t.Fatal(err) + } + + // Simulate a legacy DB: tables exist but no schema_migrations + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + t.Fatal(err) + } + if _, err := db.Exec("CREATE TABLE legacy_items (id INTEGER PRIMARY KEY); ALTER TABLE legacy_items ADD COLUMN created_at DATETIME;"); err != nil { + t.Fatal(err) + } + db.Close() + + migration := []byte("CREATE TABLE legacy_items (id INTEGER PRIMARY KEY);\nALTER TABLE legacy_items ADD COLUMN created_at DATETIME;\n") + if err := os.WriteFile(filepath.Join(migDir, "001_test.sql"), migration, 0644); err != nil { + t.Fatal(err) + } + + // Should not fail even though migration SQL would fail if re-run + store, err := New(dbPath, migDir) + if err != nil { + t.Fatalf("New() on legacy DB failed: %v", err) + } + defer store.Close() + + // Confirm migration was recorded as applied + var count int + store.db.QueryRow("SELECT COUNT(*) FROM schema_migrations WHERE filename = '001_test.sql'").Scan(&count) + if count != 1 { + t.Errorf("expected migration to be seeded in schema_migrations, got count=%d", count) + } +} + +func TestRunMigrations_IdempotentOnRestart(t *testing.T) { + tempDir := t.TempDir() + dbPath := filepath.Join(tempDir, "test.db") + migDir := filepath.Join(tempDir, "migrations") + if err := os.MkdirAll(migDir, 0755); err != nil { + t.Fatal(err) + } + + // Write a migration with a non-idempotent ALTER TABLE + migration := []byte("CREATE TABLE foo (id INTEGER PRIMARY KEY);\nALTER TABLE foo ADD COLUMN bar TEXT;\n") + if err := os.WriteFile(filepath.Join(migDir, "001_test.sql"), migration, 0644); err != nil { + t.Fatal(err) + } + + // First run — should succeed + store1, err := New(dbPath, migDir) + if err != nil { + t.Fatalf("first New() failed: %v", err) + } + store1.Close() + + // Second run (simulating service restart) — should also succeed + store2, err := New(dbPath, migDir) + if err != nil { + t.Fatalf("second New() failed (migration re-run not skipped): %v", err) + } + store2.Close() +} + // setupTestStoreWithTasks creates a test store with tasks table func setupTestStoreWithTasks(t *testing.T) *Store { t.Helper() @@ -631,126 +699,6 @@ func TestGetCardsByDateRange(t *testing.T) { } } -// setupTestStoreWithBugs creates a test store with bugs table -func setupTestStoreWithBugs(t *testing.T) *Store { - t.Helper() - - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test.db") - - db, err := sql.Open("sqlite3", dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - - db.SetMaxOpenConns(1) - - store := &Store{db: db} - - schema := ` - CREATE TABLE IF NOT EXISTS bugs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - description TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - resolved_at DATETIME DEFAULT NULL - ); - ` - if _, err := db.Exec(schema); err != nil { - t.Fatalf("Failed to create schema: %v", err) - } - - return store -} - -func TestBugResolution(t *testing.T) { - store := setupTestStoreWithBugs(t) - defer func() { _ = store.Close() }() - - // Save some bugs - if err := store.SaveBug("Bug 1"); err != nil { - t.Fatalf("Failed to save bug 1: %v", err) - } - if err := store.SaveBug("Bug 2"); err != nil { - t.Fatalf("Failed to save bug 2: %v", err) - } - if err := store.SaveBug("Bug 3"); err != nil { - t.Fatalf("Failed to save bug 3: %v", err) - } - - // Verify all 3 bugs are unresolved - unresolved, err := store.GetUnresolvedBugs() - if err != nil { - t.Fatalf("GetUnresolvedBugs failed: %v", err) - } - if len(unresolved) != 3 { - t.Errorf("Expected 3 unresolved bugs, got %d", len(unresolved)) - } - - // Resolve bug 2 - if err := store.ResolveBug(2); err != nil { - t.Fatalf("ResolveBug failed: %v", err) - } - - // Verify only 2 unresolved bugs remain - unresolved, err = store.GetUnresolvedBugs() - if err != nil { - t.Fatalf("GetUnresolvedBugs after resolve failed: %v", err) - } - if len(unresolved) != 2 { - t.Errorf("Expected 2 unresolved bugs, got %d", len(unresolved)) - } - - // Verify bug 2 is not in the unresolved list - for _, bug := range unresolved { - if bug.ID == 2 { - t.Error("Bug 2 should have been resolved but is still in unresolved list") - } - } - - // Verify all bugs still exist in GetBugs (including resolved) - allBugs, err := store.GetBugs() - if err != nil { - t.Fatalf("GetBugs failed: %v", err) - } - if len(allBugs) != 3 { - t.Errorf("Expected 3 total bugs, got %d", len(allBugs)) - } - - // Verify bug 2 has resolved_at set - for _, bug := range allBugs { - if bug.ID == 2 { - if bug.ResolvedAt == nil { - t.Error("Bug 2 should have resolved_at set") - } - } - } - - // Unresolve bug 2 - if err := store.UnresolveBug(2); err != nil { - t.Fatalf("UnresolveBug failed: %v", err) - } - - // Verify all 3 bugs are unresolved again - unresolved, err = store.GetUnresolvedBugs() - if err != nil { - t.Fatalf("GetUnresolvedBugs after unresolve failed: %v", err) - } - if len(unresolved) != 3 { - t.Errorf("Expected 3 unresolved bugs after unresolve, got %d", len(unresolved)) - } -} - -func TestResolveBug_NonExistent(t *testing.T) { - store := setupTestStoreWithBugs(t) - defer func() { _ = store.Close() }() - - // Resolving a non-existent bug should not error (no rows affected is fine) - err := store.ResolveBug(999) - if err != nil { - t.Errorf("ResolveBug on non-existent bug should not error, got: %v", err) - } -} - // ============================================================================= // User Shopping Items Tests // ============================================================================= |
