diff --git a/cmd/cascade_cli_test.go b/cmd/cascade_cli_test.go index 0f273734..ff5970b6 100644 --- a/cmd/cascade_cli_test.go +++ b/cmd/cascade_cli_test.go @@ -19,10 +19,10 @@ func TestCascadeCLIBasic(t *testing.T) { defer database.Close() epic := &models.Issue{Title: "Epic: Feature X", Type: models.TypeEpic, Status: models.StatusOpen} - database.CreateIssue(epic) + mustCreateIssue(t, database, epic) child := &models.Issue{Title: "Task: Implement", Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID} - database.CreateIssue(child) + mustCreateIssue(t, database, child) sessionID := "ses_test_cascade" @@ -30,7 +30,7 @@ func TestCascadeCLIBasic(t *testing.T) { child.Status = models.StatusClosed now := time.Now() child.ClosedAt = &now - database.UpdateIssue(child) + mustUpdateIssue(t, database, child) cascaded, cascadedIDs := database.CascadeUpParentStatus(child.ID, models.StatusClosed, sessionID) @@ -55,14 +55,14 @@ func TestCascadeCLIMultipleChildren(t *testing.T) { sessionID := "ses_multi_children" epic := &models.Issue{Title: "Epic: Big Feature", Type: models.TypeEpic, Status: models.StatusOpen} - database.CreateIssue(epic) + mustCreateIssue(t, database, epic) children := make([]*models.Issue, 3) for i := 0; i < 3; i++ { children[i] = &models.Issue{ Title: fmt.Sprintf("Child %d", i+1), Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID, } - database.CreateIssue(children[i]) + mustCreateIssue(t, database, children[i]) } // Close first two @@ -70,7 +70,7 @@ func TestCascadeCLIMultipleChildren(t *testing.T) { for i := 0; i < 2; i++ { children[i].Status = models.StatusClosed children[i].ClosedAt = &now - database.UpdateIssue(children[i]) + mustUpdateIssue(t, database, children[i]) } // Should NOT cascade yet @@ -82,7 +82,7 @@ func TestCascadeCLIMultipleChildren(t *testing.T) { // Close last child children[2].Status = models.StatusClosed children[2].ClosedAt = &now - database.UpdateIssue(children[2]) + mustUpdateIssue(t, database, children[2]) // Now cascade cascaded, _ = database.CascadeUpParentStatus(children[2].ID, models.StatusClosed, sessionID) @@ -105,18 +105,18 @@ func TestCascadeCLINestedHierarchy(t *testing.T) { sessionID := "ses_nested" grandparent := &models.Issue{Title: "Epic: L1", Type: models.TypeEpic, Status: models.StatusOpen} - database.CreateIssue(grandparent) + mustCreateIssue(t, database, grandparent) parent := &models.Issue{Title: "Epic: L2", Type: models.TypeEpic, Status: models.StatusOpen, ParentID: grandparent.ID} - database.CreateIssue(parent) + mustCreateIssue(t, database, parent) child := &models.Issue{Title: "Task: L3", Type: models.TypeTask, Status: models.StatusOpen, ParentID: parent.ID} - database.CreateIssue(child) + mustCreateIssue(t, database, child) now := time.Now() child.Status = models.StatusClosed child.ClosedAt = &now - database.UpdateIssue(child) + mustUpdateIssue(t, database, child) cascaded, _ := database.CascadeUpParentStatus(child.ID, models.StatusClosed, sessionID) @@ -143,13 +143,13 @@ func TestCascadeCLIStatusRules(t *testing.T) { sessionID := "ses_rules" epic := &models.Issue{Title: "Epic for review", Type: models.TypeEpic, Status: models.StatusOpen} - database.CreateIssue(epic) + mustCreateIssue(t, database, epic) child := &models.Issue{Title: "Child for review", Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID} - database.CreateIssue(child) + mustCreateIssue(t, database, child) child.Status = models.StatusInReview - database.UpdateIssue(child) + mustUpdateIssue(t, database, child) cascaded, _ := database.CascadeUpParentStatus(child.ID, models.StatusInReview, sessionID) @@ -170,12 +170,12 @@ func TestCascadeCLINoParent(t *testing.T) { defer database.Close() task := &models.Issue{Title: "Orphan Task", Type: models.TypeTask, Status: models.StatusOpen} - database.CreateIssue(task) + mustCreateIssue(t, database, task) now := time.Now() task.Status = models.StatusClosed task.ClosedAt = &now - database.UpdateIssue(task) + mustUpdateIssue(t, database, task) cascaded, cascadedIDs := database.CascadeUpParentStatus(task.ID, models.StatusClosed, "ses_orphan") @@ -195,15 +195,15 @@ func TestCascadeCLIUndoable(t *testing.T) { sessionID := "ses_undo_test" epic := &models.Issue{Title: "Epic for undo", Type: models.TypeEpic, Status: models.StatusOpen} - database.CreateIssue(epic) + mustCreateIssue(t, database, epic) child := &models.Issue{Title: "Child for undo", Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID} - database.CreateIssue(child) + mustCreateIssue(t, database, child) now := time.Now() child.Status = models.StatusClosed child.ClosedAt = &now - database.UpdateIssue(child) + mustUpdateIssue(t, database, child) database.CascadeUpParentStatus(child.ID, models.StatusClosed, sessionID) @@ -231,15 +231,15 @@ func TestCascadeCLINonEpicParent(t *testing.T) { defer database.Close() parentTask := &models.Issue{Title: "Parent Task", Type: models.TypeTask, Status: models.StatusOpen} - database.CreateIssue(parentTask) + mustCreateIssue(t, database, parentTask) childTask := &models.Issue{Title: "Child Task", Type: models.TypeTask, Status: models.StatusOpen, ParentID: parentTask.ID} - database.CreateIssue(childTask) + mustCreateIssue(t, database, childTask) now := time.Now() childTask.Status = models.StatusClosed childTask.ClosedAt = &now - database.UpdateIssue(childTask) + mustUpdateIssue(t, database, childTask) cascaded, _ := database.CascadeUpParentStatus(childTask.ID, models.StatusClosed, "ses_non_epic") @@ -279,14 +279,14 @@ func TestCascadeCLITableDriven(t *testing.T) { sessionID := fmt.Sprintf("ses_%s", tt.name) parent := &models.Issue{Title: fmt.Sprintf("Parent: %s", tt.name), Type: tt.parentType, Status: models.StatusOpen} - database.CreateIssue(parent) + mustCreateIssue(t, database, parent) children := make([]*models.Issue, tt.numChildren) for i := 0; i < tt.numChildren; i++ { children[i] = &models.Issue{ Title: fmt.Sprintf("Child %d", i+1), Type: models.TypeTask, Status: models.StatusOpen, ParentID: parent.ID, } - database.CreateIssue(children[i]) + mustCreateIssue(t, database, children[i]) } now := time.Now() @@ -295,7 +295,7 @@ func TestCascadeCLITableDriven(t *testing.T) { if tt.targetStatus == models.StatusClosed { children[i].ClosedAt = &now } - database.UpdateIssue(children[i]) + mustUpdateIssue(t, database, children[i]) } cascaded, _ := database.CascadeUpParentStatus(children[tt.childrenToClose-1].ID, tt.targetStatus, sessionID) @@ -322,21 +322,21 @@ func TestCascadeCLIInReviewStatus(t *testing.T) { defer database.Close() epic := &models.Issue{Title: "Epic in review", Type: models.TypeEpic, Status: models.StatusOpen} - database.CreateIssue(epic) + mustCreateIssue(t, database, epic) child1 := &models.Issue{Title: "Child 1", Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID} - database.CreateIssue(child1) + mustCreateIssue(t, database, child1) child2 := &models.Issue{Title: "Child 2", Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID} - database.CreateIssue(child2) + mustCreateIssue(t, database, child2) child1.Status = models.StatusInReview - database.UpdateIssue(child1) + mustUpdateIssue(t, database, child1) now := time.Now() child2.Status = models.StatusClosed child2.ClosedAt = &now - database.UpdateIssue(child2) + mustUpdateIssue(t, database, child2) cascaded, _ := database.CascadeUpParentStatus(child1.ID, models.StatusInReview, "ses_review") @@ -357,18 +357,18 @@ func TestCascadeCLIMixedStatusChildren(t *testing.T) { defer database.Close() epic := &models.Issue{Title: "Mixed epic", Type: models.TypeEpic, Status: models.StatusOpen} - database.CreateIssue(epic) + mustCreateIssue(t, database, epic) child1 := &models.Issue{Title: "Open", Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID} - database.CreateIssue(child1) + mustCreateIssue(t, database, child1) child2 := &models.Issue{Title: "InProgress", Type: models.TypeTask, Status: models.StatusInProgress, ParentID: epic.ID} - database.CreateIssue(child2) + mustCreateIssue(t, database, child2) child3 := &models.Issue{Title: "Closed", Type: models.TypeTask, Status: models.StatusClosed, ParentID: epic.ID} now := time.Now() child3.ClosedAt = &now - database.CreateIssue(child3) + mustCreateIssue(t, database, child3) cascaded, _ := database.CascadeUpParentStatus(child3.ID, models.StatusClosed, "ses_mixed") @@ -390,14 +390,14 @@ func TestCascadeCLIAlreadyClosed(t *testing.T) { now := time.Now() epic := &models.Issue{Title: "Closed epic", Type: models.TypeEpic, Status: models.StatusClosed, ClosedAt: &now} - database.CreateIssue(epic) + mustCreateIssue(t, database, epic) child := &models.Issue{Title: "Orphan child", Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID} - database.CreateIssue(child) + mustCreateIssue(t, database, child) child.Status = models.StatusClosed child.ClosedAt = &now - database.UpdateIssue(child) + mustUpdateIssue(t, database, child) cascaded, _ := database.CascadeUpParentStatus(child.ID, models.StatusClosed, "ses_already_closed") diff --git a/cmd/context_test.go b/cmd/context_test.go index 8755e83c..5e6e8a38 100644 --- a/cmd/context_test.go +++ b/cmd/context_test.go @@ -21,7 +21,7 @@ func TestResumeSetsFocus(t *testing.T) { Title: "Issue to resume", Status: models.StatusInProgress, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Set focus via config (simulating resume command) if err := config.SetFocus(dir, issue.ID); err != nil { @@ -50,7 +50,7 @@ func TestResumeWithInProgressIssue(t *testing.T) { Title: "In Progress Work", Status: models.StatusInProgress, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) if err := config.SetFocus(dir, issue.ID); err != nil { t.Fatalf("SetFocus failed: %v", err) @@ -84,7 +84,7 @@ func TestResumePreservesIssueState(t *testing.T) { Priority: models.PriorityP1, Points: 8, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) originalStatus := issue.Status @@ -119,24 +119,24 @@ func TestResumeMultipleIssuesSequence(t *testing.T) { issue2 := &models.Issue{Title: "Second Issue", Status: models.StatusInProgress} issue3 := &models.Issue{Title: "Third Issue", Status: models.StatusInReview} - database.CreateIssue(issue1) - database.CreateIssue(issue2) - database.CreateIssue(issue3) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) + mustCreateIssue(t, database, issue3) // Resume each in sequence - config.SetFocus(dir, issue1.ID) + mustSetFocus(t, dir, issue1.ID) focused1, _ := config.GetFocus(dir) if focused1 != issue1.ID { t.Error("Focus should be issue1") } - config.SetFocus(dir, issue2.ID) + mustSetFocus(t, dir, issue2.ID) focused2, _ := config.GetFocus(dir) if focused2 != issue2.ID { t.Error("Focus should be issue2") } - config.SetFocus(dir, issue3.ID) + mustSetFocus(t, dir, issue3.ID) focused3, _ := config.GetFocus(dir) if focused3 != issue3.ID { t.Error("Focus should be issue3") @@ -161,10 +161,10 @@ func TestResumeAllowsContextInformation(t *testing.T) { Points: 21, Labels: []string{"backend", "critical"}, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Resume and retrieve context - config.SetFocus(dir, issue.ID) + mustSetFocus(t, dir, issue.ID) retrieved, _ := database.GetIssue(issue.ID) if retrieved.ID != issue.ID { @@ -191,9 +191,9 @@ func TestResumeWithBlockedIssue(t *testing.T) { Title: "Blocked Work", Status: models.StatusBlocked, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) - config.SetFocus(dir, issue.ID) + mustSetFocus(t, dir, issue.ID) retrieved, _ := database.GetIssue(issue.ID) if retrieved.Status != models.StatusBlocked { @@ -214,10 +214,10 @@ func TestResumeWithClosedIssue(t *testing.T) { Title: "Completed Work", Status: models.StatusClosed, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Can still resume closed issue for context - config.SetFocus(dir, issue.ID) + mustSetFocus(t, dir, issue.ID) focused, _ := config.GetFocus(dir) if focused != issue.ID { @@ -254,7 +254,7 @@ func TestResumeWithLogs(t *testing.T) { Title: "Issue with History", Status: models.StatusInProgress, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Add some logs for i := 0; i < 3; i++ { @@ -264,11 +264,11 @@ func TestResumeWithLogs(t *testing.T) { Message: "Progress update", Type: models.LogTypeProgress, } - database.AddLog(log) + mustAddLog(t, database, log) } // Resume and verify logs are accessible - config.SetFocus(dir, issue.ID) + mustSetFocus(t, dir, issue.ID) logs, _ := database.GetLogs(issue.ID, 10) if len(logs) != 3 { @@ -289,17 +289,17 @@ func TestResumePreservesParentChild(t *testing.T) { Title: "Parent Epic", Type: models.TypeEpic, } - database.CreateIssue(parent) + mustCreateIssue(t, database, parent) child := &models.Issue{ Title: "Child Task", ParentID: parent.ID, Type: models.TypeTask, } - database.CreateIssue(child) + mustCreateIssue(t, database, child) // Resume child - config.SetFocus(dir, child.ID) + mustSetFocus(t, dir, child.ID) // Verify relationship preserved retrieved, _ := database.GetIssue(child.ID) @@ -320,8 +320,8 @@ func TestResumePreserveDependencies(t *testing.T) { prerequisite := &models.Issue{Title: "Prerequisite"} dependent := &models.Issue{Title: "Dependent"} - database.CreateIssue(prerequisite) - database.CreateIssue(dependent) + mustCreateIssue(t, database, prerequisite) + mustCreateIssue(t, database, dependent) // Add dependency if err := database.AddDependency(dependent.ID, prerequisite.ID, "depends_on"); err != nil { @@ -329,7 +329,7 @@ func TestResumePreserveDependencies(t *testing.T) { } // Resume dependent - config.SetFocus(dir, dependent.ID) + mustSetFocus(t, dir, dependent.ID) // Verify dependency preserved deps, _ := database.GetDependencies(dependent.ID) diff --git a/cmd/create.go b/cmd/create.go index 09f9b009..14885433 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -33,7 +33,9 @@ var createCmd = &cobra.Command{ if models.IsValidType(normalized) { typeFlag, _ := cmd.Flags().GetString("type") if typeFlag == "" { - cmd.Flags().Set("type", string(normalized)) + if err := cmd.Flags().Set("type", string(normalized)); err != nil { + return err + } } args = args[1:] } diff --git a/cmd/create_test.go b/cmd/create_test.go index 029f0c0d..830eee8b 100644 --- a/cmd/create_test.go +++ b/cmd/create_test.go @@ -278,7 +278,7 @@ func TestIssueDefaultStatus(t *testing.T) { issue := &models.Issue{ Title: "New Issue", } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if retrieved.Status != models.StatusOpen { @@ -299,13 +299,13 @@ func TestCreateMultipleDependencies(t *testing.T) { prereq1 := &models.Issue{Title: "Prereq 1"} prereq2 := &models.Issue{Title: "Prereq 2"} prereq3 := &models.Issue{Title: "Prereq 3"} - database.CreateIssue(prereq1) - database.CreateIssue(prereq2) - database.CreateIssue(prereq3) + mustCreateIssue(t, database, prereq1) + mustCreateIssue(t, database, prereq2) + mustCreateIssue(t, database, prereq3) // Create dependent issue dependent := &models.Issue{Title: "Dependent"} - database.CreateIssue(dependent) + mustCreateIssue(t, database, dependent) // Add multiple dependencies if err := database.AddDependency(dependent.ID, prereq1.ID, "depends_on"); err != nil { @@ -351,7 +351,7 @@ func TestCreateIssueIDFormat(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // ID should be "td-" + 6 hex chars = 9 total chars if !strings.HasPrefix(issue.ID, "td-") { @@ -372,7 +372,7 @@ func TestCreateIssueTimestamps(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) if issue.CreatedAt.IsZero() { t.Error("Expected CreatedAt to be set") @@ -406,7 +406,7 @@ func TestCreateNotesFlagAlias(t *testing.T) { } // Reset - createCmd.Flags().Set("notes", "") + mustSetFlag(t, createCmd.Flags(), "notes", "") } // TestCreateTagFlagParsing tests that --tag and --tags flags are defined and work diff --git a/cmd/defer.go b/cmd/defer.go index b8e05095..2b259192 100644 --- a/cmd/defer.go +++ b/cmd/defer.go @@ -49,12 +49,14 @@ var deferCmd = &cobra.Command{ return err } - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: issueID, SessionID: sess.ID, Message: "Deferral cleared", Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("failed to add log for %s: %v", issueID, err) + } fmt.Printf("DEFERRAL CLEARED %s\n", issueID) return nil @@ -88,12 +90,14 @@ var deferCmd = &cobra.Command{ logMsg = fmt.Sprintf("Deferred until %s (deferred %d times)", dateStr, issue.DeferCount) } - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: issueID, SessionID: sess.ID, Message: logMsg, Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("failed to add log for %s: %v", issueID, err) + } fmt.Printf("DEFERRED %s until %s\n", issueID, dateStr) return nil diff --git a/cmd/delete_test.go b/cmd/delete_test.go index aeee590b..c43ccca5 100644 --- a/cmd/delete_test.go +++ b/cmd/delete_test.go @@ -58,7 +58,7 @@ func TestDeleteMultipleIssues(t *testing.T) { issueIDs := make([]string, 0) for _, issue := range issues { - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) issueIDs = append(issueIDs, issue.ID) } @@ -95,7 +95,7 @@ func TestDeleteLogsAction(t *testing.T) { Title: "Test Issue", Status: models.StatusOpen, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) sessionID := "ses_test123" issueData := `{"id":"` + issue.ID + `","title":"Test Issue","status":"open"}` @@ -171,7 +171,7 @@ func TestDeleteFromDifferentStatuses(t *testing.T) { Title: tc.name, Status: tc.initialStatus, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) if err := database.DeleteIssue(issue.ID); err != nil { t.Errorf("Failed to delete from %s: %v", tc.initialStatus, err) @@ -198,10 +198,10 @@ func TestDeleteAlreadyDeletedIssue(t *testing.T) { Title: "Already Deleted", Status: models.StatusOpen, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Delete once - database.DeleteIssue(issue.ID) + mustDeleteIssue(t, database, issue.ID) // Delete again (should be idempotent or still work) err = database.DeleteIssue(issue.ID) @@ -228,11 +228,11 @@ func TestDeleteWithDependencies(t *testing.T) { parent := &models.Issue{Title: "Parent", Status: models.StatusOpen} child := &models.Issue{Title: "Child", Status: models.StatusOpen} - database.CreateIssue(parent) - database.CreateIssue(child) + mustCreateIssue(t, database, parent) + mustCreateIssue(t, database, child) // Add dependency - database.AddDependency(child.ID, parent.ID, "depends_on") + mustAddDependency(t, database, child.ID, parent.ID, "depends_on") // Delete parent if err := database.DeleteIssue(parent.ID); err != nil { @@ -271,14 +271,14 @@ func TestDeleteUpdatesTimestamp(t *testing.T) { Title: "Test Issue", Status: models.StatusOpen, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) if issue.DeletedAt != nil { t.Error("DeletedAt should be nil before delete") } // Delete - database.DeleteIssue(issue.ID) + mustDeleteIssue(t, database, issue.ID) retrieved, _ := database.GetIssue(issue.ID) if retrieved.DeletedAt == nil { @@ -304,12 +304,12 @@ func TestDeletePreservesIssueData(t *testing.T) { Points: 8, Labels: []string{"backend", "critical"}, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) issueID := issue.ID // Delete the issue - database.DeleteIssue(issueID) + mustDeleteIssue(t, database, issueID) // Retrieve and verify data is preserved retrieved, _ := database.GetIssue(issueID) @@ -356,13 +356,13 @@ func TestDeleteMultipleWithMixedStatuses(t *testing.T) { Title: string(status), Status: status, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) issueIDs = append(issueIDs, issue.ID) } // Delete all for _, id := range issueIDs { - database.DeleteIssue(id) + mustDeleteIssue(t, database, id) } // Verify all deleted @@ -386,8 +386,8 @@ func TestDeletePartialFailure(t *testing.T) { issue1 := &models.Issue{Title: "Issue 1", Status: models.StatusOpen} issue2 := &models.Issue{Title: "Issue 2", Status: models.StatusOpen} - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) // Delete first issue successfully err1 := database.DeleteIssue(issue1.ID) diff --git a/cmd/dependencies_test.go b/cmd/dependencies_test.go index f34d2a1a..5752f110 100644 --- a/cmd/dependencies_test.go +++ b/cmd/dependencies_test.go @@ -21,11 +21,11 @@ func TestWouldCreateCycleSimple(t *testing.T) { // Create two issues issue1 := &models.Issue{Title: "Issue 1", Status: models.StatusOpen} issue2 := &models.Issue{Title: "Issue 2", Status: models.StatusOpen} - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) // Add issue2 depends on issue1 - database.AddDependency(issue2.ID, issue1.ID, "depends_on") + mustAddDependency(t, database, issue2.ID, issue1.ID, "depends_on") // Check if adding issue1 depends on issue2 would create cycle if !dependency.WouldCreateCycle(database, issue1.ID, issue2.ID) { @@ -46,12 +46,12 @@ func TestWouldCreateCycleTransitive(t *testing.T) { issue1 := &models.Issue{Title: "Issue 1", Status: models.StatusOpen} issue2 := &models.Issue{Title: "Issue 2", Status: models.StatusOpen} issue3 := &models.Issue{Title: "Issue 3", Status: models.StatusOpen} - database.CreateIssue(issue1) - database.CreateIssue(issue2) - database.CreateIssue(issue3) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) + mustCreateIssue(t, database, issue3) - database.AddDependency(issue2.ID, issue1.ID, "depends_on") - database.AddDependency(issue3.ID, issue2.ID, "depends_on") + mustAddDependency(t, database, issue2.ID, issue1.ID, "depends_on") + mustAddDependency(t, database, issue3.ID, issue2.ID, "depends_on") // issue1 -> issue3 would create cycle: issue1 -> issue3 -> issue2 -> issue1 if !dependency.WouldCreateCycle(database, issue1.ID, issue3.ID) { @@ -77,12 +77,12 @@ func TestWouldCreateCycleNoCycle(t *testing.T) { issue1 := &models.Issue{Title: "Issue 1", Status: models.StatusOpen} issue2 := &models.Issue{Title: "Issue 2", Status: models.StatusOpen} issue3 := &models.Issue{Title: "Issue 3", Status: models.StatusOpen} - database.CreateIssue(issue1) - database.CreateIssue(issue2) - database.CreateIssue(issue3) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) + mustCreateIssue(t, database, issue3) // issue2 depends on issue1 (no cycle yet) - database.AddDependency(issue2.ID, issue1.ID, "depends_on") + mustAddDependency(t, database, issue2.ID, issue1.ID, "depends_on") // issue3 -> issue1 should be fine (no cycle) if dependency.WouldCreateCycle(database, issue3.ID, issue1.ID) { @@ -105,7 +105,7 @@ func TestGetTransitiveBlockedEmpty(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Standalone Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) blocked := dependency.GetTransitiveBlocked(database, issue.ID, make(map[string]bool)) if len(blocked) != 0 { @@ -126,13 +126,13 @@ func TestGetTransitiveBlockedDirect(t *testing.T) { blocker := &models.Issue{Title: "Blocker", Status: models.StatusOpen} blocked1 := &models.Issue{Title: "Blocked 1", Status: models.StatusOpen} blocked2 := &models.Issue{Title: "Blocked 2", Status: models.StatusOpen} - database.CreateIssue(blocker) - database.CreateIssue(blocked1) - database.CreateIssue(blocked2) + mustCreateIssue(t, database, blocker) + mustCreateIssue(t, database, blocked1) + mustCreateIssue(t, database, blocked2) // Both blocked1 and blocked2 depend on blocker - database.AddDependency(blocked1.ID, blocker.ID, "depends_on") - database.AddDependency(blocked2.ID, blocker.ID, "depends_on") + mustAddDependency(t, database, blocked1.ID, blocker.ID, "depends_on") + mustAddDependency(t, database, blocked2.ID, blocker.ID, "depends_on") allBlocked := dependency.GetTransitiveBlocked(database, blocker.ID, make(map[string]bool)) if len(allBlocked) != 2 { @@ -154,14 +154,14 @@ func TestGetTransitiveBlockedChain(t *testing.T) { issue2 := &models.Issue{Title: "Issue 2", Status: models.StatusOpen} issue3 := &models.Issue{Title: "Issue 3", Status: models.StatusOpen} issue4 := &models.Issue{Title: "Issue 4", Status: models.StatusOpen} - database.CreateIssue(issue1) - database.CreateIssue(issue2) - database.CreateIssue(issue3) - database.CreateIssue(issue4) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) + mustCreateIssue(t, database, issue3) + mustCreateIssue(t, database, issue4) - database.AddDependency(issue2.ID, issue1.ID, "depends_on") - database.AddDependency(issue3.ID, issue2.ID, "depends_on") - database.AddDependency(issue4.ID, issue3.ID, "depends_on") + mustAddDependency(t, database, issue2.ID, issue1.ID, "depends_on") + mustAddDependency(t, database, issue3.ID, issue2.ID, "depends_on") + mustAddDependency(t, database, issue4.ID, issue3.ID, "depends_on") // issue1 transitively blocks 3 issues allBlocked := dependency.GetTransitiveBlocked(database, issue1.ID, make(map[string]bool)) @@ -195,15 +195,15 @@ func TestGetTransitiveBlockedDiamond(t *testing.T) { mid1 := &models.Issue{Title: "Mid1", Status: models.StatusOpen} mid2 := &models.Issue{Title: "Mid2", Status: models.StatusOpen} bottom := &models.Issue{Title: "Bottom", Status: models.StatusOpen} - database.CreateIssue(top) - database.CreateIssue(mid1) - database.CreateIssue(mid2) - database.CreateIssue(bottom) + mustCreateIssue(t, database, top) + mustCreateIssue(t, database, mid1) + mustCreateIssue(t, database, mid2) + mustCreateIssue(t, database, bottom) - database.AddDependency(mid1.ID, top.ID, "depends_on") - database.AddDependency(mid2.ID, top.ID, "depends_on") - database.AddDependency(bottom.ID, mid1.ID, "depends_on") - database.AddDependency(bottom.ID, mid2.ID, "depends_on") + mustAddDependency(t, database, mid1.ID, top.ID, "depends_on") + mustAddDependency(t, database, mid2.ID, top.ID, "depends_on") + mustAddDependency(t, database, bottom.ID, mid1.ID, "depends_on") + mustAddDependency(t, database, bottom.ID, mid2.ID, "depends_on") // getTransitiveBlocked returns all paths, so bottom appears twice (via mid1 and mid2) // This is how it counts total blocking relationships, not unique issues @@ -224,7 +224,7 @@ func TestSelfReferenceDetection(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Self Loop", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Direct self-reference via WouldCreateCycle if !dependency.WouldCreateCycle(database, issue.ID, issue.ID) { @@ -260,7 +260,7 @@ func TestBuildCriticalPathSequenceSingle(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Single", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) issueMap := map[string]*models.Issue{issue.ID: issue} blockCounts := make(map[string]int) @@ -284,12 +284,12 @@ func TestBuildCriticalPathSequenceChain(t *testing.T) { issue1 := &models.Issue{Title: "Issue 1", Status: models.StatusOpen} issue2 := &models.Issue{Title: "Issue 2", Status: models.StatusOpen} issue3 := &models.Issue{Title: "Issue 3", Status: models.StatusOpen} - database.CreateIssue(issue1) - database.CreateIssue(issue2) - database.CreateIssue(issue3) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) + mustCreateIssue(t, database, issue3) - database.AddDependency(issue2.ID, issue1.ID, "depends_on") - database.AddDependency(issue3.ID, issue2.ID, "depends_on") + mustAddDependency(t, database, issue2.ID, issue1.ID, "depends_on") + mustAddDependency(t, database, issue3.ID, issue2.ID, "depends_on") issueMap := map[string]*models.Issue{ issue1.ID: issue1, @@ -323,8 +323,8 @@ func TestBuildCriticalPathSkipsClosedIssues(t *testing.T) { openIssue := &models.Issue{Title: "Open", Status: models.StatusOpen} closedIssue := &models.Issue{Title: "Closed", Status: models.StatusClosed} - database.CreateIssue(openIssue) - database.CreateIssue(closedIssue) + mustCreateIssue(t, database, openIssue) + mustCreateIssue(t, database, closedIssue) issueMap := map[string]*models.Issue{ openIssue.ID: openIssue, @@ -363,7 +363,7 @@ func TestDepAddDependsOnFlag(t *testing.T) { } // Reset - depAddCmd.Flags().Set("depends-on", "") + mustSetFlag(t, depAddCmd.Flags(), "depends-on", "") } // TestAddDependencySingle tests adding a single dependency @@ -378,8 +378,8 @@ func TestAddDependencySingle(t *testing.T) { // Create two issues issue1 := &models.Issue{Title: "Setup Database", Status: models.StatusOpen} issue2 := &models.Issue{Title: "Implement API", Status: models.StatusOpen} - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) // Add dependency: issue2 depends on issue1 err = addDependency(database, issue2.ID, issue1.ID, "ses_test") @@ -433,7 +433,7 @@ func TestAddDependencyMultiple(t *testing.T) { // Create main issue and dependency issues mainIssue := &models.Issue{Title: "Integrations", Status: models.StatusOpen} - database.CreateIssue(mainIssue) + mustCreateIssue(t, database, mainIssue) depIssueIDs := make([]string, tt.numDeps) for i := 0; i < tt.numDeps; i++ { @@ -441,7 +441,7 @@ func TestAddDependencyMultiple(t *testing.T) { Title: fmt.Sprintf("Dependency %d", i+1), Status: models.StatusOpen, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) depIssueIDs[i] = issue.ID } @@ -490,7 +490,7 @@ func TestAddDependencyCircularDetection(t *testing.T) { name: "simple cycle", setupChain: func(database *db.DB, issues []*models.Issue) { // issue1 -> issue2 - database.AddDependency(issues[1].ID, issues[0].ID, "depends_on") + mustAddDependency(t, database, issues[1].ID, issues[0].ID, "depends_on") }, cycleFrom: 0, // Try to add: issue1 depends on issue2 cycleTo: 1, @@ -501,8 +501,8 @@ func TestAddDependencyCircularDetection(t *testing.T) { name: "transitive cycle", setupChain: func(database *db.DB, issues []*models.Issue) { // issue2 -> issue1, issue3 -> issue2 - database.AddDependency(issues[1].ID, issues[0].ID, "depends_on") - database.AddDependency(issues[2].ID, issues[1].ID, "depends_on") + mustAddDependency(t, database, issues[1].ID, issues[0].ID, "depends_on") + mustAddDependency(t, database, issues[2].ID, issues[1].ID, "depends_on") }, cycleFrom: 0, // Try to add: issue1 depends on issue3 cycleTo: 2, @@ -523,7 +523,7 @@ func TestAddDependencyCircularDetection(t *testing.T) { name: "no cycle valid dep", setupChain: func(database *db.DB, issues []*models.Issue) { // issue2 -> issue1 - database.AddDependency(issues[1].ID, issues[0].ID, "depends_on") + mustAddDependency(t, database, issues[1].ID, issues[0].ID, "depends_on") }, cycleFrom: 2, // Try to add: issue3 depends on issue1 (valid) cycleTo: 0, @@ -548,7 +548,7 @@ func TestAddDependencyCircularDetection(t *testing.T) { Title: fmt.Sprintf("Issue %d", i+1), Status: models.StatusOpen, } - database.CreateIssue(issues[i]) + mustCreateIssue(t, database, issues[i]) } // Setup the dependency chain @@ -576,7 +576,7 @@ func TestAddDependencyValidation(t *testing.T) { name: "issue not found source", setup: func(database *db.DB) (string, string) { issue := &models.Issue{Title: "Exists", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) return "nonexistent", issue.ID }, wantError: true, @@ -586,7 +586,7 @@ func TestAddDependencyValidation(t *testing.T) { name: "issue not found target", setup: func(database *db.DB) (string, string) { issue := &models.Issue{Title: "Exists", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) return issue.ID, "nonexistent" }, wantError: true, @@ -597,10 +597,12 @@ func TestAddDependencyValidation(t *testing.T) { setup: func(database *db.DB) (string, string) { issue1 := &models.Issue{Title: "Issue 1", Status: models.StatusOpen} issue2 := &models.Issue{Title: "Issue 2", Status: models.StatusOpen} - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) // Add dependency first time - addDependency(database, issue1.ID, issue2.ID, "ses_test") + if err := addDependency(database, issue1.ID, issue2.ID, "ses_test"); err != nil { + t.Fatalf("addDependency failed: %v", err) + } return issue1.ID, issue2.ID }, wantError: false, // addDependency returns nil for duplicates (with warning) @@ -611,8 +613,8 @@ func TestAddDependencyValidation(t *testing.T) { setup: func(database *db.DB) (string, string) { issue1 := &models.Issue{Title: "Backend", Status: models.StatusOpen} issue2 := &models.Issue{Title: "Database", Status: models.StatusOpen} - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) return issue1.ID, issue2.ID }, wantError: false, @@ -623,8 +625,8 @@ func TestAddDependencyValidation(t *testing.T) { setup: func(database *db.DB) (string, string) { issue1 := &models.Issue{Title: "Resolved API", Status: models.StatusClosed} issue2 := &models.Issue{Title: "New Feature", Status: models.StatusOpen} - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) return issue2.ID, issue1.ID }, wantError: false, @@ -635,8 +637,8 @@ func TestAddDependencyValidation(t *testing.T) { setup: func(database *db.DB) (string, string) { issue1 := &models.Issue{Title: "In Progress", Status: models.StatusInProgress} issue2 := &models.Issue{Title: "Blocked", Status: models.StatusBlocked} - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) return issue2.ID, issue1.ID }, wantError: false, @@ -674,13 +676,17 @@ func TestAddDependencyPersistence(t *testing.T) { issue1 := &models.Issue{Title: "Step 1", Status: models.StatusOpen} issue2 := &models.Issue{Title: "Step 2", Status: models.StatusOpen} issue3 := &models.Issue{Title: "Step 3", Status: models.StatusOpen} - database.CreateIssue(issue1) - database.CreateIssue(issue2) - database.CreateIssue(issue3) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) + mustCreateIssue(t, database, issue3) // Add dependencies - addDependency(database, issue2.ID, issue1.ID, "ses_test") - addDependency(database, issue3.ID, issue2.ID, "ses_test") + if err := addDependency(database, issue2.ID, issue1.ID, "ses_test"); err != nil { + t.Fatalf("addDependency failed: %v", err) + } + if err := addDependency(database, issue3.ID, issue2.ID, "ses_test"); err != nil { + t.Fatalf("addDependency failed: %v", err) + } database.Close() @@ -716,10 +722,10 @@ func TestAddDependencyComplexGraph(t *testing.T) { name: "diamond pattern", buildGraph: func(database *db.DB, issues map[string]*models.Issue) { // A -> B, A -> C, B -> D, C -> D - database.AddDependency(issues["B"].ID, issues["A"].ID, "depends_on") - database.AddDependency(issues["C"].ID, issues["A"].ID, "depends_on") - database.AddDependency(issues["D"].ID, issues["B"].ID, "depends_on") - database.AddDependency(issues["D"].ID, issues["C"].ID, "depends_on") + mustAddDependency(t, database, issues["B"].ID, issues["A"].ID, "depends_on") + mustAddDependency(t, database, issues["C"].ID, issues["A"].ID, "depends_on") + mustAddDependency(t, database, issues["D"].ID, issues["B"].ID, "depends_on") + mustAddDependency(t, database, issues["D"].ID, issues["C"].ID, "depends_on") }, checkFunc: func(t *testing.T, database *db.DB, issues map[string]*models.Issue) { // D should have 2 dependencies @@ -740,10 +746,10 @@ func TestAddDependencyComplexGraph(t *testing.T) { name: "multi-level chain", buildGraph: func(database *db.DB, issues map[string]*models.Issue) { // A -> B -> C -> D -> E - database.AddDependency(issues["B"].ID, issues["A"].ID, "depends_on") - database.AddDependency(issues["C"].ID, issues["B"].ID, "depends_on") - database.AddDependency(issues["D"].ID, issues["C"].ID, "depends_on") - database.AddDependency(issues["E"].ID, issues["D"].ID, "depends_on") + mustAddDependency(t, database, issues["B"].ID, issues["A"].ID, "depends_on") + mustAddDependency(t, database, issues["C"].ID, issues["B"].ID, "depends_on") + mustAddDependency(t, database, issues["D"].ID, issues["C"].ID, "depends_on") + mustAddDependency(t, database, issues["E"].ID, issues["D"].ID, "depends_on") }, checkFunc: func(t *testing.T, database *db.DB, issues map[string]*models.Issue) { // Each should have exactly 1 direct dependency @@ -765,11 +771,11 @@ func TestAddDependencyComplexGraph(t *testing.T) { name: "fan-out pattern", buildGraph: func(database *db.DB, issues map[string]*models.Issue) { // A blocks B, C, D, E, F - database.AddDependency(issues["B"].ID, issues["A"].ID, "depends_on") - database.AddDependency(issues["C"].ID, issues["A"].ID, "depends_on") - database.AddDependency(issues["D"].ID, issues["A"].ID, "depends_on") - database.AddDependency(issues["E"].ID, issues["A"].ID, "depends_on") - database.AddDependency(issues["F"].ID, issues["A"].ID, "depends_on") + mustAddDependency(t, database, issues["B"].ID, issues["A"].ID, "depends_on") + mustAddDependency(t, database, issues["C"].ID, issues["A"].ID, "depends_on") + mustAddDependency(t, database, issues["D"].ID, issues["A"].ID, "depends_on") + mustAddDependency(t, database, issues["E"].ID, issues["A"].ID, "depends_on") + mustAddDependency(t, database, issues["F"].ID, issues["A"].ID, "depends_on") }, checkFunc: func(t *testing.T, database *db.DB, issues map[string]*models.Issue) { // Each of B-F should have exactly 1 dependency on A @@ -805,7 +811,7 @@ func TestAddDependencyComplexGraph(t *testing.T) { Title: fmt.Sprintf("Issue %s", label), Status: models.StatusOpen, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) issues[label] = issue } diff --git a/cmd/due.go b/cmd/due.go index f3293ea2..4f098738 100644 --- a/cmd/due.go +++ b/cmd/due.go @@ -49,12 +49,14 @@ var dueCmd = &cobra.Command{ return err } - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: issueID, SessionID: sess.ID, Message: "Due date cleared", Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("failed to add log for %s: %v", issueID, err) + } fmt.Printf("DUE DATE CLEARED %s\n", issueID) } else { @@ -75,12 +77,14 @@ var dueCmd = &cobra.Command{ return err } - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: issueID, SessionID: sess.ID, Message: "Due date set: " + dateStr, Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("failed to add log for %s: %v", issueID, err) + } fmt.Printf("DUE DATE SET %s: %s\n", issueID, dateStr) } diff --git a/cmd/epic.go b/cmd/epic.go index 29dfe4f9..d8731036 100644 --- a/cmd/epic.go +++ b/cmd/epic.go @@ -110,7 +110,9 @@ func init() { epicCreateCmd.Flags().StringArray("blocks", nil, "Issues this blocks (repeatable, comma-separated)") // Hidden type flag - set programmatically to "epic" epicCreateCmd.Flags().StringP("type", "t", "", "") - epicCreateCmd.Flags().MarkHidden("type") + if err := epicCreateCmd.Flags().MarkHidden("type"); err != nil { + panic(err) + } // epicListCmd flags epicListCmd.Flags().BoolP("all", "a", false, "Show all epics including closed") diff --git a/cmd/epic_test.go b/cmd/epic_test.go index b042f9d4..880cb902 100644 --- a/cmd/epic_test.go +++ b/cmd/epic_test.go @@ -88,5 +88,5 @@ func TestEpicCreateHasHiddenTypeFlag(t *testing.T) { } // Reset - epicCreateCmd.Flags().Set("type", "") + mustSetFlag(t, epicCreateCmd.Flags(), "type", "") } diff --git a/cmd/focus_test.go b/cmd/focus_test.go index 3a7eda78..0f71d2ae 100644 --- a/cmd/focus_test.go +++ b/cmd/focus_test.go @@ -54,18 +54,18 @@ func TestFocusChangeFocus(t *testing.T) { issue1 := &models.Issue{Title: "Issue 1"} issue2 := &models.Issue{Title: "Issue 2"} - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) // Focus on issue 1 - config.SetFocus(dir, issue1.ID) + mustSetFocus(t, dir, issue1.ID) focused1, _ := config.GetFocus(dir) if focused1 != issue1.ID { t.Errorf("First focus failed: expected %s, got %s", issue1.ID, focused1) } // Change focus to issue 2 - config.SetFocus(dir, issue2.ID) + mustSetFocus(t, dir, issue2.ID) focused2, _ := config.GetFocus(dir) if focused2 != issue2.ID { t.Errorf("Focus change failed: expected %s, got %s", issue2.ID, focused2) @@ -103,10 +103,10 @@ func TestUnfocus(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Set focus - config.SetFocus(dir, issue.ID) + mustSetFocus(t, dir, issue.ID) focused, _ := config.GetFocus(dir) if focused != issue.ID { t.Error("Focus not set correctly") @@ -146,7 +146,7 @@ func TestFocusWithDifferentStatuses(t *testing.T) { Title: string(status), Status: status, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Focus on this issue if err := config.SetFocus(dir, issue.ID); err != nil { @@ -173,10 +173,10 @@ func TestFocusPersistence(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Set focus - config.SetFocus(dir, issue.ID) + mustSetFocus(t, dir, issue.ID) // Close and reopen database database.Close() @@ -205,7 +205,7 @@ func TestFocusFileCreation(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Set focus if err := config.SetFocus(dir, issue.ID); err != nil { @@ -233,7 +233,7 @@ func TestFocusMultipleIssuesSequential(t *testing.T) { for i := 0; i < issueCount; i++ { issue := &models.Issue{Title: "Issue " + string(rune(i))} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) issues[i] = issue } @@ -294,12 +294,12 @@ func TestFocusIDFormat(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) originalID := issue.ID // Set focus - config.SetFocus(dir, originalID) + mustSetFocus(t, dir, originalID) // Verify ID is preserved exactly focused, _ := config.GetFocus(dir) @@ -318,10 +318,10 @@ func TestFocusWhitespace(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Set focus - config.SetFocus(dir, issue.ID) + mustSetFocus(t, dir, issue.ID) // Verify no whitespace issues focused, _ := config.GetFocus(dir) @@ -340,7 +340,7 @@ func TestFocusWithSpecialCharacters(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // IDs should be alphanumeric format like "td-xxxxx" if len(issue.ID) == 0 || issue.ID[:3] != "td-" { @@ -348,7 +348,7 @@ func TestFocusWithSpecialCharacters(t *testing.T) { } // Set focus - config.SetFocus(dir, issue.ID) + mustSetFocus(t, dir, issue.ID) focused, _ := config.GetFocus(dir) if focused != issue.ID { @@ -369,15 +369,15 @@ func TestFocusConcurrentChanges(t *testing.T) { issue2 := &models.Issue{Title: "Issue 2"} issue3 := &models.Issue{Title: "Issue 3"} - database.CreateIssue(issue1) - database.CreateIssue(issue2) - database.CreateIssue(issue3) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) + mustCreateIssue(t, database, issue3) // Rapidly change focus for i := 0; i < 3; i++ { - config.SetFocus(dir, issue1.ID) - config.SetFocus(dir, issue2.ID) - config.SetFocus(dir, issue3.ID) + mustSetFocus(t, dir, issue1.ID) + mustSetFocus(t, dir, issue2.ID) + mustSetFocus(t, dir, issue3.ID) } // Final focus should be issue 3 diff --git a/cmd/handoff.go b/cmd/handoff.go index 348d5b72..8d2d5618 100644 --- a/cmd/handoff.go +++ b/cmd/handoff.go @@ -216,12 +216,14 @@ Or use flags with values, stdin (-), or file (@path): } // Add log entry for visibility - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: child.ID, SessionID: sess.ID, Message: fmt.Sprintf("Cascaded handoff from %s", issueID), Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("cascade handoff log %s: %v", child.ID, err) + } cascaded++ } diff --git a/cmd/handoff_test.go b/cmd/handoff_test.go index f7a6e5b9..d8b4e429 100644 --- a/cmd/handoff_test.go +++ b/cmd/handoff_test.go @@ -31,7 +31,7 @@ func TestHandoffRecordsData(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue", Status: models.StatusInProgress} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) handoff := &models.Handoff{ IssueID: issue.ID, @@ -61,7 +61,7 @@ func TestHandoffRecordsGitSnapshot(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue", Status: models.StatusInProgress} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Add handoff handoff := &models.Handoff{ @@ -69,7 +69,7 @@ func TestHandoffRecordsGitSnapshot(t *testing.T) { SessionID: "ses_test", Done: []string{"Work done"}, } - database.AddHandoff(handoff) + mustAddHandoff(t, database, handoff) // Record git snapshot snapshot := &models.GitSnapshot{ @@ -133,11 +133,11 @@ func TestHandoffUpdatesIssueTimestamp(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue", Status: models.StatusInProgress} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) originalUpdatedAt := issue.UpdatedAt // Update issue (as handoff command would) - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if retrieved.UpdatedAt.Before(originalUpdatedAt) { @@ -296,7 +296,7 @@ func TestMultipleHandoffsForSameIssue(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue", Status: models.StatusInProgress} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // First handoff handoff1 := &models.Handoff{ @@ -304,7 +304,7 @@ func TestMultipleHandoffsForSameIssue(t *testing.T) { SessionID: "ses_1", Done: []string{"First work"}, } - database.AddHandoff(handoff1) + mustAddHandoff(t, database, handoff1) // Second handoff handoff2 := &models.Handoff{ @@ -312,7 +312,7 @@ func TestMultipleHandoffsForSameIssue(t *testing.T) { SessionID: "ses_2", Done: []string{"Second work"}, } - database.AddHandoff(handoff2) + mustAddHandoff(t, database, handoff2) if handoff1.ID == handoff2.ID { t.Error("Handoffs should have different IDs") @@ -345,7 +345,7 @@ func TestHandoffNoteFlag(t *testing.T) { } // Reset - handoffCmd.Flags().Set("note", "") + mustSetFlag(t, handoffCmd.Flags(), "note", "") } // TestGetLatestHandoff tests retrieving the most recent handoff @@ -358,16 +358,16 @@ func TestGetLatestHandoff(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue", Status: models.StatusInProgress} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Add handoffs - database.AddHandoff(&models.Handoff{ + mustAddHandoff(t, database, &models.Handoff{ IssueID: issue.ID, SessionID: "ses_old", Done: []string{"Old work"}, }) - database.AddHandoff(&models.Handoff{ + mustAddHandoff(t, database, &models.Handoff{ IssueID: issue.ID, SessionID: "ses_new", Done: []string{"New work"}, @@ -441,7 +441,7 @@ func TestHandoffMessageFlag(t *testing.T) { } // Reset - handoffCmd.Flags().Set("message", "") + mustSetFlag(t, handoffCmd.Flags(), "message", "") } // TestCascadeHandoffBasic tests that handoff cascades to children diff --git a/cmd/init.go b/cmd/init.go index 4b7132b5..de54e656 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -81,10 +81,14 @@ func addToGitignore(path string) { // Add newline if file doesn't end with one if len(contentStr) > 0 && !strings.HasSuffix(contentStr, "\n") { - f.WriteString("\n") + if _, err := f.WriteString("\n"); err != nil { + return + } } - f.WriteString(".todos/\n") + if _, err := f.WriteString(".todos/\n"); err != nil { + return + } fmt.Println("Added .todos/ to .gitignore") } diff --git a/cmd/link.go b/cmd/link.go index e30ef000..ebf24198 100644 --- a/cmd/link.go +++ b/cmd/link.go @@ -209,7 +209,7 @@ Examples: if info.IsDir() { if recursive { - filepath.Walk(match, func(path string, info os.FileInfo, err error) error { + if err := filepath.Walk(match, func(path string, info os.FileInfo, err error) error { if err != nil { return nil } @@ -217,7 +217,9 @@ Examples: allFiles = append(allFiles, path) } return nil - }) + }); err != nil { + output.Warning("failed to walk %s: %v", match, err) + } } else { // Just files in the directory entries, _ := os.ReadDir(match) diff --git a/cmd/lint_helpers_test.go b/cmd/lint_helpers_test.go new file mode 100644 index 00000000..fb4c23d3 --- /dev/null +++ b/cmd/lint_helpers_test.go @@ -0,0 +1,115 @@ +package cmd + +import ( + "testing" + + "github.com/marcus/td/internal/config" + "github.com/marcus/td/internal/db" + "github.com/marcus/td/internal/models" + "github.com/spf13/pflag" +) + +func mustSetFocus(t *testing.T, dir, issueID string) { + t.Helper() + if err := config.SetFocus(dir, issueID); err != nil { + t.Fatalf("SetFocus(%q): %v", issueID, err) + } +} + +func mustCreateIssue(t *testing.T, database *db.DB, issue *models.Issue) { + t.Helper() + if err := database.CreateIssue(issue); err != nil { + t.Fatalf("CreateIssue(%q): %v", issue.Title, err) + } +} + +func mustUpdateIssue(t *testing.T, database *db.DB, issue *models.Issue) { + t.Helper() + if err := database.UpdateIssue(issue); err != nil { + t.Fatalf("UpdateIssue(%q): %v", issue.ID, err) + } +} + +func mustDeleteIssue(t *testing.T, database *db.DB, issueID string) { + t.Helper() + if err := database.DeleteIssue(issueID); err != nil { + t.Fatalf("DeleteIssue(%q): %v", issueID, err) + } +} + +func mustAddDependency(t *testing.T, database *db.DB, issueID, dependsOnID, relationType string) { + t.Helper() + if err := database.AddDependency(issueID, dependsOnID, relationType); err != nil { + t.Fatalf("AddDependency(%q, %q): %v", issueID, dependsOnID, err) + } +} + +func mustAddHandoff(t *testing.T, database *db.DB, handoff *models.Handoff) { + t.Helper() + if err := database.AddHandoff(handoff); err != nil { + t.Fatalf("AddHandoff(%q): %v", handoff.IssueID, err) + } +} + +func mustCreateWorkSession(t *testing.T, database *db.DB, ws *models.WorkSession) { + t.Helper() + if err := database.CreateWorkSession(ws); err != nil { + t.Fatalf("CreateWorkSession(%q): %v", ws.Name, err) + } +} + +func mustAddLog(t *testing.T, database *db.DB, log *models.Log) { + t.Helper() + if err := database.AddLog(log); err != nil { + t.Fatalf("AddLog(%q): %v", log.IssueID, err) + } +} + +func mustRemoveDependency(t *testing.T, database *db.DB, issueID, dependsOnID string) { + t.Helper() + if err := database.RemoveDependency(issueID, dependsOnID); err != nil { + t.Fatalf("RemoveDependency(%q, %q): %v", issueID, dependsOnID, err) + } +} + +func mustSetActiveWorkSession(t *testing.T, dir, wsID string) { + t.Helper() + if err := config.SetActiveWorkSession(dir, wsID); err != nil { + t.Fatalf("SetActiveWorkSession(%q): %v", wsID, err) + } +} + +func mustClearActiveWorkSession(t *testing.T, dir string) { + t.Helper() + if err := config.ClearActiveWorkSession(dir); err != nil { + t.Fatalf("ClearActiveWorkSession: %v", err) + } +} + +func mustTagIssueToWorkSession(t *testing.T, database *db.DB, wsID, issueID, sessionID string) { + t.Helper() + if err := database.TagIssueToWorkSession(wsID, issueID, sessionID); err != nil { + t.Fatalf("TagIssueToWorkSession(%q, %q): %v", wsID, issueID, err) + } +} + +func mustUntagIssueFromWorkSession(t *testing.T, database *db.DB, wsID, issueID, sessionID string) { + t.Helper() + if err := database.UntagIssueFromWorkSession(wsID, issueID, sessionID); err != nil { + t.Fatalf("UntagIssueFromWorkSession(%q, %q): %v", wsID, issueID, err) + } +} + +func mustSetFlag(t *testing.T, flags *pflag.FlagSet, name, value string) { + t.Helper() + if err := flags.Set(name, value); err != nil { + t.Fatalf("Set(%q): %v", name, err) + } +} + +func mustSetFlagValue(t *testing.T, flag *pflag.Flag, value string) { + t.Helper() + if err := flag.Value.Set(value); err != nil { + t.Fatalf("Set flag value for %q: %v", flag.Name, err) + } +} diff --git a/cmd/log_test.go b/cmd/log_test.go index 80ade049..2ed87b77 100644 --- a/cmd/log_test.go +++ b/cmd/log_test.go @@ -20,9 +20,7 @@ func TestLogSingleMessage(t *testing.T) { Title: "Test Issue", Status: models.StatusOpen, } - if err := database.CreateIssue(issue); err != nil { - t.Fatalf("CreateIssue failed: %v", err) - } + mustCreateIssue(t, database, issue) message := "Started working on this" log := &models.Log{ @@ -32,9 +30,7 @@ func TestLogSingleMessage(t *testing.T) { Type: models.LogTypeProgress, } - if err := database.AddLog(log); err != nil { - t.Fatalf("AddLog failed: %v", err) - } + mustAddLog(t, database, log) logs, _ := database.GetLogs(issue.ID, 10) if len(logs) != 1 { @@ -55,7 +51,7 @@ func TestLogMultipleMessages(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) messages := []string{ "Initial exploration", @@ -71,7 +67,7 @@ func TestLogMultipleMessages(t *testing.T) { Message: msg, Type: models.LogTypeProgress, } - database.AddLog(log) + mustAddLog(t, database, log) } logs, _ := database.GetLogs(issue.ID, 10) @@ -102,7 +98,7 @@ func TestLogWithDifferentTypes(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) testCases := []struct { name string @@ -124,9 +120,7 @@ func TestLogWithDifferentTypes(t *testing.T) { Message: tc.message, Type: tc.logType, } - if err := database.AddLog(log); err != nil { - t.Fatalf("AddLog for %s failed: %v", tc.name, err) - } + mustAddLog(t, database, log) } logs, _ := database.GetLogs(issue.ID, 10) @@ -157,7 +151,7 @@ func TestLogRetrievalLimit(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Add 10 logs for i := 0; i < 10; i++ { @@ -167,7 +161,7 @@ func TestLogRetrievalLimit(t *testing.T) { Message: "Message " + string(rune(i)), Type: models.LogTypeProgress, } - database.AddLog(log) + mustAddLog(t, database, log) } // Test different limits @@ -201,8 +195,8 @@ func TestLogForMultipleIssues(t *testing.T) { issue1 := &models.Issue{Title: "Issue 1"} issue2 := &models.Issue{Title: "Issue 2"} - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) // Add logs to issue 1 for i := 0; i < 3; i++ { @@ -212,7 +206,7 @@ func TestLogForMultipleIssues(t *testing.T) { Message: "Issue 1 log", Type: models.LogTypeProgress, } - database.AddLog(log) + mustAddLog(t, database, log) } // Add logs to issue 2 @@ -223,7 +217,7 @@ func TestLogForMultipleIssues(t *testing.T) { Message: "Issue 2 log", Type: models.LogTypeProgress, } - database.AddLog(log) + mustAddLog(t, database, log) } logs1, _ := database.GetLogs(issue1.ID, 10) @@ -259,7 +253,7 @@ func TestLogWithMultipleSessions(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) sessions := []string{"ses_aaa", "ses_bbb", "ses_ccc"} @@ -270,7 +264,7 @@ func TestLogWithMultipleSessions(t *testing.T) { Message: "Log from " + session, Type: models.LogTypeProgress, } - database.AddLog(log) + mustAddLog(t, database, log) } logs, _ := database.GetLogs(issue.ID, 10) @@ -300,7 +294,7 @@ func TestLogWithWorkSession(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) workSessionID := "ws_12345" log := &models.Log{ @@ -311,9 +305,7 @@ func TestLogWithWorkSession(t *testing.T) { Type: models.LogTypeProgress, } - if err := database.AddLog(log); err != nil { - t.Fatalf("AddLog failed: %v", err) - } + mustAddLog(t, database, log) logs, _ := database.GetLogs(issue.ID, 10) if len(logs) != 1 { @@ -334,7 +326,7 @@ func TestLogEmptyMessage(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) log := &models.Log{ IssueID: issue.ID, @@ -360,7 +352,7 @@ func TestLogLongMessage(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Create a long message longMessage := "" @@ -375,9 +367,7 @@ func TestLogLongMessage(t *testing.T) { Type: models.LogTypeProgress, } - if err := database.AddLog(log); err != nil { - t.Fatalf("AddLog failed: %v", err) - } + mustAddLog(t, database, log) logs, _ := database.GetLogs(issue.ID, 10) if len(logs) != 1 { @@ -421,7 +411,7 @@ func TestLogRetrieval(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Add logs in specific order messages := []string{"First", "Second", "Third"} @@ -432,7 +422,7 @@ func TestLogRetrieval(t *testing.T) { Message: msg, Type: models.LogTypeProgress, } - database.AddLog(log) + mustAddLog(t, database, log) } logs, _ := database.GetLogs(issue.ID, 10) diff --git a/cmd/review.go b/cmd/review.go index 89b7bb4f..4e8c38ed 100644 --- a/cmd/review.go +++ b/cmd/review.go @@ -19,7 +19,9 @@ import ( func clearFocusIfNeeded(baseDir, issueID string) { focusedID, _ := config.GetFocus(baseDir) if focusedID == issueID { - config.ClearFocus(baseDir) + if err := config.ClearFocus(baseDir); err != nil { + output.Warning("failed to clear focus for %s: %v", issueID, err) + } } } @@ -677,12 +679,14 @@ Supports bulk operations: } logMsg = fmt.Sprintf("[%s] Approved (CREATOR EXCEPTION: %s)", agentInfo, reason) logType = models.LogTypeSecurity - db.LogSecurityEvent(baseDir, db.SecurityEvent{ + if err := db.LogSecurityEvent(baseDir, db.SecurityEvent{ IssueID: issueID, SessionID: sess.ID, AgentType: sess.AgentType, Reason: "creator_approval_exception: " + reason, - }) + }); err != nil { + output.Warning("failed to record security event for %s: %v", issueID, err) + } } if err := database.AddLog(&models.Log{ @@ -844,7 +848,9 @@ Supports bulk operations: if reason != "" { result["reason"] = reason } - output.JSON(result) + if err := output.JSON(result); err != nil { + return err + } } else { fmt.Printf("REJECTED %s → open\n", issueID) } @@ -979,12 +985,14 @@ Examples: logType = models.LogTypeSecurity // Also log to the separate security audit file - db.LogSecurityEvent(baseDir, db.SecurityEvent{ + if err := db.LogSecurityEvent(baseDir, db.SecurityEvent{ IssueID: issueID, SessionID: sess.ID, AgentType: sess.AgentType, Reason: selfCloseException, - }) + }); err != nil { + output.Warning("failed to record security event for %s: %v", issueID, err) + } } else if reason != "" { logMsg = "Closed: " + reason } diff --git a/cmd/review_test.go b/cmd/review_test.go index 2b861989..24420f7e 100644 --- a/cmd/review_test.go +++ b/cmd/review_test.go @@ -684,7 +684,7 @@ func TestCascadeReviewNestedEpics(t *testing.T) { // Mark all for review for _, d := range descendants { d.Status = models.StatusInReview - database.UpdateIssue(d) + mustUpdateIssue(t, database, d) } // Verify all are in_review @@ -750,7 +750,7 @@ func TestCascadeUpToReviewAllChildrenReview(t *testing.T) { // Now mark child2 as in_review child2.Status = models.StatusInReview - database.UpdateIssue(child2) + mustUpdateIssue(t, database, child2) // Cascade up should now update epic cascaded, _ := database.CascadeUpParentStatus(child2.ID, models.StatusInReview, sessionID) @@ -1048,7 +1048,7 @@ func TestReviewMinorFlag(t *testing.T) { } // Reset - reviewCmd.Flags().Set("minor", "false") + mustSetFlag(t, reviewCmd.Flags(), "minor", "false") } func TestReviewReasonShorthand(t *testing.T) { @@ -1071,7 +1071,7 @@ func TestReviewReasonShorthand(t *testing.T) { } // Reset - reviewCmd.Flags().Set("reason", "") + mustSetFlag(t, reviewCmd.Flags(), "reason", "") } func TestApproveReasonShorthand(t *testing.T) { @@ -1135,7 +1135,7 @@ func TestCloseSelfCloseExceptionRequiresValue(t *testing.T) { } // Reset flag to default before test - flag.Value.Set("") + mustSetFlagValue(t, flag, "") // Set a test value if err := flag.Value.Set("test reason"); err != nil { @@ -1148,7 +1148,7 @@ func TestCloseSelfCloseExceptionRequiresValue(t *testing.T) { } // Reset for other tests - flag.Value.Set("") + mustSetFlagValue(t, flag, "") } func TestCloseSelfCloseScenarios(t *testing.T) { @@ -1174,7 +1174,7 @@ func TestCloseSelfCloseScenarios(t *testing.T) { if err := database.CreateIssue(issueWithImpl); err != nil { t.Fatalf("CreateIssue failed: %v", err) } - database.UpdateIssue(issueWithImpl) + mustUpdateIssue(t, database, issueWithImpl) retrieved, _ := database.GetIssue(issueWithImpl.ID) if retrieved.ImplementerSession != sessionID { @@ -1235,13 +1235,13 @@ func TestCloseSelfCloseExceptionLogMessage(t *testing.T) { if err := database.CreateIssue(issue); err != nil { t.Fatalf("CreateIssue failed: %v", err) } - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) // Simulate closing with exception - manually add the log entry exceptionReason := "trivial typo fix" logMsg := "[test-agent] Closed (SELF-CLOSE EXCEPTION: " + exceptionReason + ")" - database.AddLog(&models.Log{ + mustAddLog(t, database, &models.Log{ IssueID: issue.ID, SessionID: sessionID, Message: logMsg, @@ -1693,19 +1693,19 @@ func TestApproveAutoUnblocksDependents(t *testing.T) { Status: models.StatusInReview, ImplementerSession: "ses_impl", } - database.CreateIssue(blocker) + mustCreateIssue(t, database, blocker) // Create dependent (blocked, depends on blocker) dependent := &models.Issue{ Title: "Dependent", Status: models.StatusBlocked, } - database.CreateIssue(dependent) - database.AddDependency(dependent.ID, blocker.ID, "depends_on") + mustCreateIssue(t, database, dependent) + mustAddDependency(t, database, dependent.ID, blocker.ID, "depends_on") // Simulate approve: close the blocker then cascade unblock blocker.Status = models.StatusClosed - database.UpdateIssue(blocker) + mustUpdateIssue(t, database, blocker) database.CascadeUnblockDependents(blocker.ID, "ses_reviewer") // Verify dependent is now open @@ -1727,18 +1727,18 @@ func TestCloseAutoUnblocksDependents(t *testing.T) { Title: "Blocker", Status: models.StatusOpen, } - database.CreateIssue(blocker) + mustCreateIssue(t, database, blocker) dependent := &models.Issue{ Title: "Dependent", Status: models.StatusBlocked, } - database.CreateIssue(dependent) - database.AddDependency(dependent.ID, blocker.ID, "depends_on") + mustCreateIssue(t, database, dependent) + mustAddDependency(t, database, dependent.ID, blocker.ID, "depends_on") // Simulate close: set closed then cascade unblock blocker.Status = models.StatusClosed - database.UpdateIssue(blocker) + mustUpdateIssue(t, database, blocker) database.CascadeUnblockDependents(blocker.ID, "ses_closer") updated, _ := database.GetIssue(dependent.ID) @@ -1768,15 +1768,15 @@ func TestApproveAutoUnblockPartialDeps(t *testing.T) { Title: "Dependent", Status: models.StatusBlocked, } - database.CreateIssue(a1) - database.CreateIssue(a2) - database.CreateIssue(dependent) - database.AddDependency(dependent.ID, a1.ID, "depends_on") - database.AddDependency(dependent.ID, a2.ID, "depends_on") + mustCreateIssue(t, database, a1) + mustCreateIssue(t, database, a2) + mustCreateIssue(t, database, dependent) + mustAddDependency(t, database, dependent.ID, a1.ID, "depends_on") + mustAddDependency(t, database, dependent.ID, a2.ID, "depends_on") // Approve only A1 a1.Status = models.StatusClosed - database.UpdateIssue(a1) + mustUpdateIssue(t, database, a1) database.CascadeUnblockDependents(a1.ID, "ses_reviewer") // Dependent should still be blocked (A2 not closed) diff --git a/cmd/root.go b/cmd/root.go index a5d11ffd..51442254 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -156,7 +156,9 @@ func logAgentError(args []string, errMsg string) { } // Log the error (silently fails if project not initialized) - db.LogAgentError(dir, args, errMsg, sessionID) + if err := db.LogAgentError(dir, args, errMsg, sessionID); err != nil { + fmt.Fprintf(os.Stderr, "warning: failed to log agent error: %v\n", err) + } } // handleUnknownFlagError checks if error is an unknown flag and suggests alternatives diff --git a/cmd/search_test.go b/cmd/search_test.go index 0d60efdc..6a559df9 100644 --- a/cmd/search_test.go +++ b/cmd/search_test.go @@ -25,8 +25,8 @@ func TestSearchByTitle(t *testing.T) { Status: models.StatusOpen, } - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) // Search for "login" opts := db.ListIssuesOptions{ @@ -67,7 +67,7 @@ func TestSearchByDescription(t *testing.T) { Description: "Database connection pool is exhausted", Status: models.StatusOpen, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) opts := db.ListIssuesOptions{ Search: "database", @@ -113,8 +113,8 @@ func TestSearchByLabel(t *testing.T) { Status: models.StatusOpen, } - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) opts := db.ListIssuesOptions{ Search: "backend", @@ -154,7 +154,7 @@ func TestSearchNoResults(t *testing.T) { Title: "Test Issue", Status: models.StatusOpen, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) opts := db.ListIssuesOptions{ Search: "nonexistent_keyword_xyz", @@ -187,8 +187,8 @@ func TestSearchWithStatusFilter(t *testing.T) { Status: models.StatusClosed, } - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) opts := db.ListIssuesOptions{ Search: "issue", @@ -227,8 +227,8 @@ func TestSearchWithTypeFilter(t *testing.T) { Status: models.StatusOpen, } - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) opts := db.ListIssuesOptions{ Search: "feature", @@ -267,8 +267,8 @@ func TestSearchWithPriorityFilter(t *testing.T) { Status: models.StatusOpen, } - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) opts := db.ListIssuesOptions{ Search: "bug", @@ -300,7 +300,7 @@ func TestSearchWithLimit(t *testing.T) { Title: "Test issue", Status: models.StatusOpen, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) } opts := db.ListIssuesOptions{ @@ -339,8 +339,8 @@ func TestSearchRelevanceScoring(t *testing.T) { Status: models.StatusOpen, } - database.CreateIssue(exactMatch) - database.CreateIssue(descMatch) + mustCreateIssue(t, database, exactMatch) + mustCreateIssue(t, database, descMatch) opts := db.ListIssuesOptions{ Search: "database query", @@ -375,7 +375,7 @@ func TestSearchCaseInsensitive(t *testing.T) { Title: "FIX LOGIN BUTTON", Status: models.StatusOpen, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) opts1 := db.ListIssuesOptions{ Search: "fix login", @@ -410,8 +410,8 @@ func TestSearchMultipleKeywords(t *testing.T) { Status: models.StatusOpen, } - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) opts := db.ListIssuesOptions{ Search: "database", @@ -442,7 +442,7 @@ func TestSearchEmptyQuery(t *testing.T) { Title: "Test Issue", Status: models.StatusOpen, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) opts := db.ListIssuesOptions{ Search: "", @@ -470,7 +470,7 @@ func TestSearchSpecialCharacters(t *testing.T) { Title: "Fix bug in auth_service.go", Status: models.StatusOpen, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) opts := db.ListIssuesOptions{ Search: "auth_service", @@ -501,7 +501,7 @@ func TestSearchWithMultipleFilters(t *testing.T) { Status: models.StatusOpen, Labels: []string{"backend", "critical"}, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) opts := db.ListIssuesOptions{ Search: "database", diff --git a/cmd/security_test.go b/cmd/security_test.go index 15c12277..66da0002 100644 --- a/cmd/security_test.go +++ b/cmd/security_test.go @@ -75,7 +75,9 @@ func TestCloseCommandSecurityLogging(t *testing.T) { defer os.RemoveAll(baseDir) // Ensure .todos directory exists for session - os.MkdirAll(filepath.Join(baseDir, ".todos"), 0755) + if err := os.MkdirAll(filepath.Join(baseDir, ".todos"), 0755); err != nil { + t.Fatal(err) + } // Init DB database, err := db.Initialize(baseDir) diff --git a/cmd/show_test.go b/cmd/show_test.go index 20ac04a3..f2fc5862 100644 --- a/cmd/show_test.go +++ b/cmd/show_test.go @@ -37,7 +37,7 @@ func TestShowFormatFlagParsing(t *testing.T) { } // Reset - showCmd.Flags().Set("format", "") + mustSetFlag(t, showCmd.Flags(), "format", "") } // TestShowAcceptsZeroArgs tests that show can be called with no arguments @@ -80,7 +80,7 @@ func TestShowJSONFlagStillWorks(t *testing.T) { } // Reset - showCmd.Flags().Set("json", "false") + mustSetFlag(t, showCmd.Flags(), "json", "false") } // TestShowRenderMarkdownFlagExists tests that --render-markdown flag is defined diff --git a/cmd/start.go b/cmd/start.go index cd0795e2..1e54df85 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -129,22 +129,26 @@ Examples: logMsg = reason } - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: issueID, SessionID: sess.ID, Message: logMsg, Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("failed to add log for %s: %v", issueID, err) + } // Record git snapshot if gitErr == nil { - database.AddGitSnapshot(&models.GitSnapshot{ + if err := database.AddGitSnapshot(&models.GitSnapshot{ IssueID: issueID, Event: "start", CommitSHA: gitState.CommitSHA, Branch: gitState.Branch, DirtyFiles: gitState.DirtyFiles, - }) + }); err != nil { + output.Warning("failed to record git snapshot for %s: %v", issueID, err) + } } fmt.Printf("STARTED %s (session: %s)\n", issueID, sess.ID) @@ -153,7 +157,9 @@ Examples: // Set focus to first issue if single issue, or clear if multiple if len(args) == 1 && started == 1 { - config.SetFocus(baseDir, args[0]) + if err := config.SetFocus(baseDir, args[0]); err != nil { + output.Warning("failed to set focus to %s: %v", args[0], err) + } } // Show git state once at the end diff --git a/cmd/start_test.go b/cmd/start_test.go index f4918555..ebacccd6 100644 --- a/cmd/start_test.go +++ b/cmd/start_test.go @@ -56,14 +56,14 @@ func TestStartMultipleIssues(t *testing.T) { } for _, issue := range issues { - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) } // Start all issues for _, issue := range issues { issue.Status = models.StatusInProgress issue.ImplementerSession = "ses_test" - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) } // Verify all started @@ -88,7 +88,7 @@ func TestStartBlockedIssueWithoutForce(t *testing.T) { Title: "Blocked Issue", Status: models.StatusBlocked, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Without force, blocked issues should remain blocked // In the actual command this would skip, here we test the state check @@ -113,14 +113,14 @@ func TestStartBlockedIssueWithForce(t *testing.T) { Title: "Blocked Issue", Status: models.StatusBlocked, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // With force, even blocked issues can be started force := true if issue.Status == models.StatusBlocked && force { issue.Status = models.StatusInProgress issue.ImplementerSession = "ses_test" - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) } retrieved, _ := database.GetIssue(issue.ID) @@ -142,12 +142,12 @@ func TestStartSetsImplementerSession(t *testing.T) { Title: "Test Issue", Status: models.StatusOpen, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) sessionID := "ses_abc123" issue.Status = models.StatusInProgress issue.ImplementerSession = sessionID - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if retrieved.ImplementerSession != sessionID { @@ -181,11 +181,11 @@ func TestStartFromDifferentStatuses(t *testing.T) { Title: tc.name, Status: tc.initialStatus, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) if tc.canStart || tc.initialStatus != models.StatusBlocked { issue.Status = models.StatusInProgress - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if retrieved.Status != models.StatusInProgress { @@ -221,7 +221,7 @@ func TestStartRecordsGitSnapshot(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Record a git snapshot snapshot := &models.GitSnapshot{ @@ -265,7 +265,7 @@ func TestStartLogsAction(t *testing.T) { } issue := &models.Issue{Title: "Test Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) sessionID := "ses_test123" @@ -305,7 +305,7 @@ func TestStartAddsProgressLog(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Add progress log as start command would log := &models.Log{ @@ -341,7 +341,7 @@ func TestStartWithReason(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) reason := "Picking up from previous session" log := &models.Log{ @@ -350,7 +350,7 @@ func TestStartWithReason(t *testing.T) { Message: reason, Type: models.LogTypeProgress, } - database.AddLog(log) + mustAddLog(t, database, log) logs, _ := database.GetLogs(issue.ID, 10) if len(logs) != 1 || logs[0].Message != reason { @@ -368,7 +368,7 @@ func TestStartMixedValidAndInvalid(t *testing.T) { defer database.Close() validIssue := &models.Issue{Title: "Valid", Status: models.StatusOpen} - database.CreateIssue(validIssue) + mustCreateIssue(t, database, validIssue) // Try to get a non-existent issue _, err = database.GetIssue("td-invalid") @@ -378,7 +378,7 @@ func TestStartMixedValidAndInvalid(t *testing.T) { // Valid issue can still be started validIssue.Status = models.StatusInProgress - database.UpdateIssue(validIssue) + mustUpdateIssue(t, database, validIssue) retrieved, _ := database.GetIssue(validIssue.ID) if retrieved.Status != models.StatusInProgress { diff --git a/cmd/sync.go b/cmd/sync.go index 83225987..73392f87 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -222,7 +222,9 @@ func runBootstrap(database *db.DB, client *syncclient.Client, state *db.SyncStat // Write snapshot if err := os.WriteFile(dbPath, snapshot.Data, 0644); err != nil { - os.Rename(backupPath, dbPath) + if restoreErr := os.Rename(backupPath, dbPath); restoreErr != nil { + err = fmt.Errorf("%w (restore backup: %v)", err, restoreErr) + } reopened, reopenErr := db.Open(baseDir) if reopenErr != nil { return nil, fmt.Errorf("write failed (%w) and reopen failed: %v", err, reopenErr) @@ -233,7 +235,9 @@ func runBootstrap(database *db.DB, client *syncclient.Client, state *db.SyncStat // Reopen and update sync_state reopened, err := db.Open(baseDir) if err != nil { - os.Rename(backupPath, dbPath) + if restoreErr := os.Rename(backupPath, dbPath); restoreErr != nil { + err = fmt.Errorf("%w (restore backup: %v)", err, restoreErr) + } reopened2, reopenErr := db.Open(baseDir) if reopenErr != nil { return nil, fmt.Errorf("reopen failed (%w) and restore reopen failed: %v", err, reopenErr) @@ -249,7 +253,9 @@ func runBootstrap(database *db.DB, client *syncclient.Client, state *db.SyncStat ) if err != nil { reopened.Close() - os.Rename(backupPath, dbPath) + if restoreErr := os.Rename(backupPath, dbPath); restoreErr != nil { + err = fmt.Errorf("%w (restore backup: %v)", err, restoreErr) + } reopened2, reopenErr := db.Open(baseDir) if reopenErr != nil { return nil, fmt.Errorf("sync_state update failed (%w) and restore reopen failed: %v", err, reopenErr) @@ -316,7 +322,7 @@ func runPush(database *db.DB, client *syncclient.Client, state *db.SyncState, de output.Error("begin tx: %v", err) return err } - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() events, err := tdsync.GetPendingEvents(tx, deviceID, sess.ID) if err != nil { @@ -492,21 +498,21 @@ func runPull(database *db.DB, client *syncclient.Client, state *db.SyncState, de result, err := tdsync.ApplyRemoteEvents(tx, events, deviceID, syncEntityValidator, state.LastSyncAt) if err != nil { - tx.Rollback() + _ = tx.Rollback() output.Error("apply events: %v", err) return err } // Store conflict records if err := storeConflicts(tx, result.Conflicts); err != nil { - tx.Rollback() + _ = tx.Rollback() output.Error("store conflicts: %v", err) return err } // Update sync_state within the same transaction to avoid race if _, err := tx.Exec(`UPDATE sync_state SET last_pulled_server_seq = ?, last_sync_at = CURRENT_TIMESTAMP`, pullResp.LastServerSeq); err != nil { - tx.Rollback() + _ = tx.Rollback() output.Error("update sync state: %v", err) return err } diff --git a/cmd/sync_tail_test.go b/cmd/sync_tail_test.go index 8ab8cb79..712c70bb 100644 --- a/cmd/sync_tail_test.go +++ b/cmd/sync_tail_test.go @@ -85,7 +85,7 @@ func TestPrintSyncEntry(t *testing.T) { DeviceID: "", Timestamp: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC), }, - contains: []string{"pull", "comments", "c_short", "delete", "seq:7"}, + contains: []string{"pull", "comments", "c_short", "delete", "seq:7"}, }, } @@ -102,7 +102,9 @@ func TestPrintSyncEntry(t *testing.T) { os.Stdout = old var buf bytes.Buffer - io.Copy(&buf, r) + if _, err := io.Copy(&buf, r); err != nil { + t.Fatalf("io.Copy failed: %v", err) + } output := buf.String() for _, s := range tt.contains { diff --git a/cmd/system.go b/cmd/system.go index 786010d3..a2f48ab5 100644 --- a/cmd/system.go +++ b/cmd/system.go @@ -557,11 +557,11 @@ var importCmd = &cobra.Command{ // exportedItem matches the JSON structure produced by the export command. type exportedItem struct { - Issue models.Issue `json:"issue"` - Logs []models.Log `json:"logs"` - Handoffs []models.Handoff `json:"handoffs"` - Dependencies []models.IssueDependency `json:"dependencies"` - Files []models.IssueFile `json:"files"` + Issue models.Issue `json:"issue"` + Logs []models.Log `json:"logs"` + Handoffs []models.Handoff `json:"handoffs"` + Dependencies []models.IssueDependency `json:"dependencies"` + Files []models.IssueFile `json:"files"` } // UnmarshalJSON supports backward-compatible deserialization: @@ -814,8 +814,9 @@ func importMarkdown(database *db.DB, data string, dryRun, force bool, sessionID } if matches := pointsRegex.FindStringSubmatch(line); matches != nil { var pts int - fmt.Sscanf(matches[1], "%d", &pts) - currentIssue.Points = pts + if _, err := fmt.Sscanf(matches[1], "%d", &pts); err == nil { + currentIssue.Points = pts + } inDescription = false continue } diff --git a/cmd/task.go b/cmd/task.go index 0b658c77..6db23b72 100644 --- a/cmd/task.go +++ b/cmd/task.go @@ -111,7 +111,9 @@ func init() { taskCreateCmd.Flags().Bool("minor", false, "Mark as minor task (allows self-review)") // Hidden type flag - set programmatically to "task" taskCreateCmd.Flags().StringP("type", "t", "", "") - taskCreateCmd.Flags().MarkHidden("type") + if err := taskCreateCmd.Flags().MarkHidden("type"); err != nil { + panic(err) + } // taskListCmd flags taskListCmd.Flags().BoolP("all", "a", false, "Show all tasks including closed") diff --git a/cmd/tree_test.go b/cmd/tree_test.go index a21eddf2..16352d38 100644 --- a/cmd/tree_test.go +++ b/cmd/tree_test.go @@ -21,7 +21,7 @@ func TestTreeSingleIssue(t *testing.T) { Type: models.TypeEpic, Status: models.StatusOpen, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Retrieve and verify retrieved, err := database.GetIssue(issue.ID) @@ -46,7 +46,7 @@ func TestTreeParentChild(t *testing.T) { Title: "Parent Epic", Type: models.TypeEpic, } - database.CreateIssue(parent) + mustCreateIssue(t, database, parent) child1 := &models.Issue{ Title: "Child Task 1", @@ -59,8 +59,8 @@ func TestTreeParentChild(t *testing.T) { Type: models.TypeTask, } - database.CreateIssue(child1) - database.CreateIssue(child2) + mustCreateIssue(t, database, child1) + mustCreateIssue(t, database, child2) // Verify parent-child relationships child1Retrieved, _ := database.GetIssue(child1.ID) @@ -89,18 +89,18 @@ func TestTreeNestedHierarchy(t *testing.T) { level2 := &models.Issue{Title: "Level 2", Type: models.TypeEpic} level3 := &models.Issue{Title: "Level 3", Type: models.TypeTask} - database.CreateIssue(root) - database.CreateIssue(level1) - database.CreateIssue(level2) - database.CreateIssue(level3) + mustCreateIssue(t, database, root) + mustCreateIssue(t, database, level1) + mustCreateIssue(t, database, level2) + mustCreateIssue(t, database, level3) level1.ParentID = root.ID level2.ParentID = level1.ID level3.ParentID = level2.ID - database.UpdateIssue(level1) - database.UpdateIssue(level2) - database.UpdateIssue(level3) + mustUpdateIssue(t, database, level1) + mustUpdateIssue(t, database, level2) + mustUpdateIssue(t, database, level3) // Verify hierarchy l1, _ := database.GetIssue(level1.ID) @@ -129,7 +129,7 @@ func TestTreeMultipleChildren(t *testing.T) { defer database.Close() parent := &models.Issue{Title: "Parent", Type: models.TypeEpic} - database.CreateIssue(parent) + mustCreateIssue(t, database, parent) // Create 5 children and track their IDs childCount := 5 @@ -141,7 +141,7 @@ func TestTreeMultipleChildren(t *testing.T) { ParentID: parent.ID, Type: models.TypeTask, } - database.CreateIssue(child) + mustCreateIssue(t, database, child) childIDs[i] = child.ID } @@ -172,18 +172,18 @@ func TestTreeWithDifferentTypes(t *testing.T) { bug := &models.Issue{Title: "Bug", Type: models.TypeBug} task := &models.Issue{Title: "Task", Type: models.TypeTask} - database.CreateIssue(epic) - database.CreateIssue(feature) - database.CreateIssue(bug) - database.CreateIssue(task) + mustCreateIssue(t, database, epic) + mustCreateIssue(t, database, feature) + mustCreateIssue(t, database, bug) + mustCreateIssue(t, database, task) feature.ParentID = epic.ID bug.ParentID = epic.ID task.ParentID = epic.ID - database.UpdateIssue(feature) - database.UpdateIssue(bug) - database.UpdateIssue(task) + mustUpdateIssue(t, database, feature) + mustUpdateIssue(t, database, bug) + mustUpdateIssue(t, database, task) // Verify hierarchy f, _ := database.GetIssue(feature.ID) @@ -209,7 +209,7 @@ func TestTreeOrphanedChildren(t *testing.T) { ParentID: "td-nonexistent", Type: models.TypeTask, } - database.CreateIssue(orphan) + mustCreateIssue(t, database, orphan) // Verify orphan exists even though parent doesn't retrieved, _ := database.GetIssue(orphan.ID) @@ -235,15 +235,15 @@ func TestTreeReparenting(t *testing.T) { parent1 := &models.Issue{Title: "Parent 1", Type: models.TypeEpic} parent2 := &models.Issue{Title: "Parent 2", Type: models.TypeEpic} - database.CreateIssue(parent1) - database.CreateIssue(parent2) + mustCreateIssue(t, database, parent1) + mustCreateIssue(t, database, parent2) child := &models.Issue{ Title: "Child", ParentID: parent1.ID, Type: models.TypeTask, } - database.CreateIssue(child) + mustCreateIssue(t, database, child) // Verify initial parent c1, _ := database.GetIssue(child.ID) @@ -253,7 +253,7 @@ func TestTreeReparenting(t *testing.T) { // Change parent child.ParentID = parent2.ID - database.UpdateIssue(child) + mustUpdateIssue(t, database, child) // Verify new parent c2, _ := database.GetIssue(child.ID) @@ -276,7 +276,7 @@ func TestTreeWithDifferentStatuses(t *testing.T) { Type: models.TypeEpic, Status: models.StatusInProgress, } - database.CreateIssue(parent) + mustCreateIssue(t, database, parent) statuses := []models.Status{ models.StatusOpen, @@ -293,7 +293,7 @@ func TestTreeWithDifferentStatuses(t *testing.T) { Type: models.TypeTask, Status: status, } - database.CreateIssue(child) + mustCreateIssue(t, database, child) retrieved, _ := database.GetIssue(child.ID) if retrieved.Status != status { @@ -315,7 +315,7 @@ func TestTreeEmptyParent(t *testing.T) { Title: "Empty Parent", Type: models.TypeEpic, } - database.CreateIssue(parent) + mustCreateIssue(t, database, parent) retrieved, _ := database.GetIssue(parent.ID) if retrieved.ID != parent.ID { @@ -337,7 +337,7 @@ func TestTreeWithPriorities(t *testing.T) { Type: models.TypeEpic, Priority: models.PriorityP0, } - database.CreateIssue(parent) + mustCreateIssue(t, database, parent) priorities := []models.Priority{ models.PriorityP0, @@ -354,7 +354,7 @@ func TestTreeWithPriorities(t *testing.T) { Type: models.TypeTask, Priority: priority, } - database.CreateIssue(child) + mustCreateIssue(t, database, child) retrieved, _ := database.GetIssue(child.ID) if retrieved.Priority != priority { @@ -376,7 +376,7 @@ func TestTreeWithPoints(t *testing.T) { Title: "Parent", Type: models.TypeEpic, } - database.CreateIssue(parent) + mustCreateIssue(t, database, parent) points := []int{1, 2, 3, 5, 8, 13, 21} @@ -387,7 +387,7 @@ func TestTreeWithPoints(t *testing.T) { Type: models.TypeTask, Points: pts, } - database.CreateIssue(child) + mustCreateIssue(t, database, child) retrieved, _ := database.GetIssue(child.ID) if retrieved.Points != pts { @@ -409,17 +409,17 @@ func TestTreeDeleteParent(t *testing.T) { Title: "Parent", Type: models.TypeEpic, } - database.CreateIssue(parent) + mustCreateIssue(t, database, parent) child := &models.Issue{ Title: "Child", ParentID: parent.ID, Type: models.TypeTask, } - database.CreateIssue(child) + mustCreateIssue(t, database, child) // Delete parent - database.DeleteIssue(parent.ID) + mustDeleteIssue(t, database, parent.ID) // Verify parent is deleted pDeleted, _ := database.GetIssue(parent.ID) @@ -460,8 +460,8 @@ func TestTreeBlockedParent(t *testing.T) { Status: models.StatusOpen, } - database.CreateIssue(parent) - database.CreateIssue(child) + mustCreateIssue(t, database, parent) + mustCreateIssue(t, database, child) pRetrieved, _ := database.GetIssue(parent.ID) if pRetrieved.Status != models.StatusBlocked { @@ -484,7 +484,7 @@ func TestTreeLargeHierarchy(t *testing.T) { defer database.Close() root := &models.Issue{Title: "Root", Type: models.TypeEpic} - database.CreateIssue(root) + mustCreateIssue(t, database, root) // Create 100 child issues for i := 0; i < 100; i++ { @@ -493,7 +493,7 @@ func TestTreeLargeHierarchy(t *testing.T) { ParentID: root.ID, Type: models.TypeTask, } - database.CreateIssue(child) + mustCreateIssue(t, database, child) } // Verify root exists diff --git a/cmd/undo_test.go b/cmd/undo_test.go index 19edcb15..2e6ea923 100644 --- a/cmd/undo_test.go +++ b/cmd/undo_test.go @@ -356,8 +356,8 @@ func TestUndoDependencyAdd(t *testing.T) { // Create two issues issue1 := &models.Issue{Title: "Issue 1", Status: models.StatusOpen} issue2 := &models.Issue{Title: "Issue 2", Status: models.StatusOpen} - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) // Add dependency if err := database.AddDependency(issue1.ID, issue2.ID, "depends_on"); err != nil { @@ -406,8 +406,8 @@ func TestUndoDependencyRemove(t *testing.T) { // Create two issues issue1 := &models.Issue{Title: "Issue 1", Status: models.StatusOpen} issue2 := &models.Issue{Title: "Issue 2", Status: models.StatusOpen} - database.CreateIssue(issue1) - database.CreateIssue(issue2) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) // Create action log for remove dependency (dependency was removed) depInfo := struct { @@ -450,7 +450,7 @@ func TestUndoFileLinkAdd(t *testing.T) { // Create issue issue := &models.Issue{Title: "Test Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Link a file if err := database.LinkFile(issue.ID, "test.go", models.FileRoleImplementation, "abc123"); err != nil { @@ -502,7 +502,7 @@ func TestUndoFileLinkRemove(t *testing.T) { // Create issue issue := &models.Issue{Title: "Test Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Create action log for unlink linkInfo := struct { @@ -549,7 +549,7 @@ func TestPerformUndoDispatch(t *testing.T) { // Create an issue for testing issue := &models.Issue{Title: "Test Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) tests := []struct { name string @@ -591,7 +591,7 @@ func TestUndoUpdateWithoutPreviousData(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) action := &models.ActionLog{ SessionID: "ses_test", @@ -617,7 +617,7 @@ func TestUndoWithInvalidPreviousData(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) action := &models.ActionLog{ SessionID: "ses_test", diff --git a/cmd/unstart.go b/cmd/unstart.go index c8e1f196..4539d711 100644 --- a/cmd/unstart.go +++ b/cmd/unstart.go @@ -89,12 +89,14 @@ Examples: logMsg = reason } - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: issueID, SessionID: sess.ID, Message: logMsg, Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("failed to add log for %s: %v", issueID, err) + } // Clear focus if this was the focused issue clearFocusIfNeeded(baseDir, issueID) diff --git a/cmd/unstart_test.go b/cmd/unstart_test.go index f4d60770..0ff39bc8 100644 --- a/cmd/unstart_test.go +++ b/cmd/unstart_test.go @@ -40,5 +40,5 @@ func TestUnstartReasonFlag(t *testing.T) { } // Reset - unstartCmd.Flags().Set("reason", "") + mustSetFlag(t, unstartCmd.Flags(), "reason", "") } diff --git a/cmd/update.go b/cmd/update.go index c6f7b8fe..8ca6a74c 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -202,22 +202,30 @@ var updateCmd = &cobra.Command{ if cmd.Flags().Changed("depends-on") { existingDeps, _ := database.GetDependencies(issueID) for _, dep := range existingDeps { - database.RemoveDependencyLogged(issueID, dep, sess.ID) + if err := database.RemoveDependencyLogged(issueID, dep, sess.ID); err != nil { + output.Warning("failed to remove dependency %s -> %s: %v", issueID, dep, err) + } } dependsArr, _ := cmd.Flags().GetStringArray("depends-on") for _, dep := range mergeMultiValueFlag(dependsArr) { - database.AddDependencyLogged(issueID, dep, "depends_on", sess.ID) + if err := database.AddDependencyLogged(issueID, dep, "depends_on", sess.ID); err != nil { + output.Warning("failed to add dependency %s -> %s: %v", issueID, dep, err) + } } } if cmd.Flags().Changed("blocks") { blocked, _ := database.GetBlockedBy(issueID) for _, b := range blocked { - database.RemoveDependencyLogged(b, issueID, sess.ID) + if err := database.RemoveDependencyLogged(b, issueID, sess.ID); err != nil { + output.Warning("failed to remove dependency %s -> %s: %v", b, issueID, err) + } } blocksArr, _ := cmd.Flags().GetStringArray("blocks") for _, b := range mergeMultiValueFlag(blocksArr) { - database.AddDependencyLogged(b, issueID, "depends_on", sess.ID) + if err := database.AddDependencyLogged(b, issueID, "depends_on", sess.ID); err != nil { + output.Warning("failed to add dependency %s -> %s: %v", b, issueID, err) + } } } @@ -271,7 +279,9 @@ func init() { updateCmd.Flags().String("status", "", "New status (open, in_progress, in_review, blocked, closed)") updateCmd.Flags().StringP("comment", "m", "", "Add a comment to the updated issue(s)") updateCmd.Flags().StringP("note", "c", "", "Alias for --comment") - updateCmd.Flags().MarkHidden("note") + if err := updateCmd.Flags().MarkHidden("note"); err != nil { + panic(err) + } updateCmd.Flags().String("defer", "", "Defer until date (e.g., +7d, monday, 2026-03-01; empty to clear)") updateCmd.Flags().String("due", "", "Due date (e.g., friday, +2w, 2026-03-15; empty to clear)") } diff --git a/cmd/update_test.go b/cmd/update_test.go index c185fbe3..1bb579d5 100644 --- a/cmd/update_test.go +++ b/cmd/update_test.go @@ -20,7 +20,7 @@ func TestUpdateIssueTitle(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Original Title"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) issue.Title = "Updated Title" if err := database.UpdateIssue(issue); err != nil { @@ -43,10 +43,10 @@ func TestUpdateIssueDescription(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test", Description: "Original desc"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) issue.Description = "New description" - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if retrieved.Description != "New description" { @@ -64,10 +64,10 @@ func TestUpdateIssueType(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test", Type: models.TypeTask} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) issue.Type = models.TypeBug - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if retrieved.Type != models.TypeBug { @@ -85,10 +85,10 @@ func TestUpdateIssuePriority(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test", Priority: models.PriorityP2} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) issue.Priority = models.PriorityP0 - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if retrieved.Priority != models.PriorityP0 { @@ -106,10 +106,10 @@ func TestUpdateIssuePoints(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test", Points: 3} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) issue.Points = 8 - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if retrieved.Points != 8 { @@ -127,10 +127,10 @@ func TestUpdateIssueLabels(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test", Labels: []string{"old"}} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) issue.Labels = []string{"new1", "new2"} - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if len(retrieved.Labels) != 2 { @@ -148,10 +148,10 @@ func TestUpdateIssueClearLabels(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test", Labels: []string{"tag1", "tag2"}} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) issue.Labels = nil - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if len(retrieved.Labels) != 0 { @@ -169,11 +169,11 @@ func TestUpdateIssueStatus(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Open -> In Progress issue.Status = models.StatusInProgress - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if retrieved.Status != models.StatusInProgress { @@ -182,7 +182,7 @@ func TestUpdateIssueStatus(t *testing.T) { // In Progress -> In Review issue.Status = models.StatusInReview - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ = database.GetIssue(issue.ID) if retrieved.Status != models.StatusInReview { @@ -203,12 +203,12 @@ func TestUpdateReplaceDependencies(t *testing.T) { issue := &models.Issue{Title: "Main Issue"} dep1 := &models.Issue{Title: "Old Dep"} dep2 := &models.Issue{Title: "New Dep"} - database.CreateIssue(issue) - database.CreateIssue(dep1) - database.CreateIssue(dep2) + mustCreateIssue(t, database, issue) + mustCreateIssue(t, database, dep1) + mustCreateIssue(t, database, dep2) // Add original dependency - database.AddDependency(issue.ID, dep1.ID, "depends_on") + mustAddDependency(t, database, issue.ID, dep1.ID, "depends_on") // Verify original deps, _ := database.GetDependencies(issue.ID) @@ -217,8 +217,8 @@ func TestUpdateReplaceDependencies(t *testing.T) { } // Replace with new dependency - database.RemoveDependency(issue.ID, dep1.ID) - database.AddDependency(issue.ID, dep2.ID, "depends_on") + mustRemoveDependency(t, database, issue.ID, dep1.ID) + mustAddDependency(t, database, issue.ID, dep2.ID, "depends_on") // Verify replacement deps, _ = database.GetDependencies(issue.ID) @@ -239,17 +239,17 @@ func TestUpdateClearDependencies(t *testing.T) { issue := &models.Issue{Title: "Main Issue"} dep1 := &models.Issue{Title: "Dep 1"} dep2 := &models.Issue{Title: "Dep 2"} - database.CreateIssue(issue) - database.CreateIssue(dep1) - database.CreateIssue(dep2) + mustCreateIssue(t, database, issue) + mustCreateIssue(t, database, dep1) + mustCreateIssue(t, database, dep2) - database.AddDependency(issue.ID, dep1.ID, "depends_on") - database.AddDependency(issue.ID, dep2.ID, "depends_on") + mustAddDependency(t, database, issue.ID, dep1.ID, "depends_on") + mustAddDependency(t, database, issue.ID, dep2.ID, "depends_on") // Clear all dependencies deps, _ := database.GetDependencies(issue.ID) for _, dep := range deps { - database.RemoveDependency(issue.ID, dep) + mustRemoveDependency(t, database, issue.ID, dep) } // Verify cleared @@ -271,16 +271,16 @@ func TestUpdateReplaceBlocks(t *testing.T) { blocker := &models.Issue{Title: "Blocker"} blocked1 := &models.Issue{Title: "Blocked 1"} blocked2 := &models.Issue{Title: "Blocked 2"} - database.CreateIssue(blocker) - database.CreateIssue(blocked1) - database.CreateIssue(blocked2) + mustCreateIssue(t, database, blocker) + mustCreateIssue(t, database, blocked1) + mustCreateIssue(t, database, blocked2) // blocked1 depends on blocker - database.AddDependency(blocked1.ID, blocker.ID, "depends_on") + mustAddDependency(t, database, blocked1.ID, blocker.ID, "depends_on") // Replace: remove blocked1, add blocked2 - database.RemoveDependency(blocked1.ID, blocker.ID) - database.AddDependency(blocked2.ID, blocker.ID, "depends_on") + mustRemoveDependency(t, database, blocked1.ID, blocker.ID) + mustAddDependency(t, database, blocked2.ID, blocker.ID, "depends_on") // Verify blockedBy, _ := database.GetBlockedBy(blocker.ID) @@ -301,14 +301,14 @@ func TestUpdateBatchMultipleIssues(t *testing.T) { issue1 := &models.Issue{Title: "Issue 1", Priority: models.PriorityP3} issue2 := &models.Issue{Title: "Issue 2", Priority: models.PriorityP3} issue3 := &models.Issue{Title: "Issue 3", Priority: models.PriorityP3} - database.CreateIssue(issue1) - database.CreateIssue(issue2) - database.CreateIssue(issue3) + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) + mustCreateIssue(t, database, issue3) // Batch update priorities for _, issue := range []*models.Issue{issue1, issue2, issue3} { issue.Priority = models.PriorityP1 - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) } // Verify all updated @@ -336,11 +336,11 @@ func TestUpdatePartialFields(t *testing.T) { Priority: models.PriorityP2, Points: 5, } - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Only update title issue.Title = "Updated Title" - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if retrieved.Title != "Updated Title" { @@ -372,13 +372,13 @@ func TestUpdateParentID(t *testing.T) { parent1 := &models.Issue{Title: "Parent 1", Type: models.TypeEpic} parent2 := &models.Issue{Title: "Parent 2", Type: models.TypeEpic} child := &models.Issue{Title: "Child", ParentID: ""} - database.CreateIssue(parent1) - database.CreateIssue(parent2) - database.CreateIssue(child) + mustCreateIssue(t, database, parent1) + mustCreateIssue(t, database, parent2) + mustCreateIssue(t, database, child) // Set initial parent child.ParentID = parent1.ID - database.UpdateIssue(child) + mustUpdateIssue(t, database, child) retrieved, _ := database.GetIssue(child.ID) if retrieved.ParentID != parent1.ID { @@ -387,7 +387,7 @@ func TestUpdateParentID(t *testing.T) { // Change parent child.ParentID = parent2.ID - database.UpdateIssue(child) + mustUpdateIssue(t, database, child) retrieved, _ = database.GetIssue(child.ID) if retrieved.ParentID != parent2.ID { @@ -396,7 +396,7 @@ func TestUpdateParentID(t *testing.T) { // Clear parent child.ParentID = "" - database.UpdateIssue(child) + mustUpdateIssue(t, database, child) retrieved, _ = database.GetIssue(child.ID) if retrieved.ParentID != "" { @@ -414,13 +414,13 @@ func TestUpdateUpdatedAtTimestamp(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) originalUpdatedAt := issue.UpdatedAt // Update issue issue.Title = "Updated" - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if !retrieved.UpdatedAt.After(originalUpdatedAt) && !retrieved.UpdatedAt.Equal(originalUpdatedAt) { @@ -438,10 +438,10 @@ func TestUpdateAcceptanceCriteria(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test", Acceptance: "Original AC"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) issue.Acceptance = "New acceptance criteria" - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if retrieved.Acceptance != "New acceptance criteria" { @@ -459,12 +459,12 @@ func TestAppendDescription(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test", Description: "Initial description"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Simulate append mode: concat with double newline newDesc := "Appended text" issue.Description = issue.Description + "\n\n" + newDesc - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) expected := "Initial description\n\nAppended text" @@ -483,11 +483,11 @@ func TestAppendToEmptyDescription(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test", Description: ""} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // With empty existing description, just set the value (no leading separator) issue.Description = "New description" - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if retrieved.Description != "New description" { @@ -505,12 +505,12 @@ func TestAppendAcceptance(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test", Acceptance: "Initial criteria"} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Simulate append mode newAC := "Additional criteria" issue.Acceptance = issue.Acceptance + "\n\n" + newAC - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) expected := "Initial criteria\n\nAdditional criteria" @@ -529,11 +529,11 @@ func TestAppendToEmptyAcceptance(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test", Acceptance: ""} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // With empty existing acceptance, just set the value issue.Acceptance = "New criteria" - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) retrieved, _ := database.GetIssue(issue.ID) if retrieved.Acceptance != "New criteria" { @@ -562,7 +562,7 @@ func TestUpdateCmdHasStatusFlag(t *testing.T) { } // Reset - updateCmd.Flags().Set("status", "") + mustSetFlag(t, updateCmd.Flags(), "status", "") } func TestUpdateRichTextAppendFromFileAndStdin(t *testing.T) { diff --git a/cmd/ws.go b/cmd/ws.go index dbdbecf2..d675cf28 100644 --- a/cmd/ws.go +++ b/cmd/ws.go @@ -75,7 +75,10 @@ var wsStartCmd = &cobra.Command{ } // Set as active - config.SetActiveWorkSession(baseDir, ws.ID) + if err := config.SetActiveWorkSession(baseDir, ws.ID); err != nil { + output.Error("failed to activate work session: %v", err) + return err + } fmt.Printf("WORK SESSION STARTED: %s\n", ws.ID) fmt.Printf("Name: %s\n", name) @@ -141,30 +144,39 @@ var wsTagCmd = &cobra.Command{ } issue.Status = models.StatusInProgress issue.ImplementerSession = sess.ID - database.UpdateIssueLogged(issue, sess.ID, models.ActionStart) + if err := database.UpdateIssueLogged(issue, sess.ID, models.ActionStart); err != nil { + output.Warning("failed to start %s: %v", issueID, err) + continue + } // Record session action for bypass prevention - database.RecordSessionAction(issueID, sess.ID, models.ActionSessionStarted) + if err := database.RecordSessionAction(issueID, sess.ID, models.ActionSessionStarted); err != nil { + output.Warning("failed to record session action for %s: %v", issueID, err) + } // Log the start - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: issueID, SessionID: sess.ID, WorkSessionID: wsID, Message: "Started (via work session)", Type: models.LogTypeProgress, - }) + }); err != nil { + output.Warning("failed to add log for %s: %v", issueID, err) + } // Capture git state gitState, _ := git.GetState() if gitState != nil { - database.AddGitSnapshot(&models.GitSnapshot{ + if err := database.AddGitSnapshot(&models.GitSnapshot{ IssueID: issueID, Event: "start", CommitSHA: gitState.CommitSHA, Branch: gitState.Branch, DirtyFiles: gitState.DirtyFiles, - }) + }); err != nil { + output.Warning("failed to capture git snapshot for %s: %v", issueID, err) + } } fmt.Printf("STARTED %s (session: %s)\n", issueID, sess.ID) @@ -266,23 +278,27 @@ var wsLogCmd = &cobra.Command{ only, _ := cmd.Flags().GetString("only") if only != "" { // Log to specific issue only - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: only, SessionID: sess.ID, WorkSessionID: wsID, Message: args[0], Type: logType, - }) + }); err != nil { + output.Warning("failed to add work-session log for %s: %v", only, err) + } fmt.Printf("LOGGED %s%s → %s\n", wsID, typeLabel, only) } else { // Store single log entry with work_session_id, no issue_id - database.AddLog(&models.Log{ + if err := database.AddLog(&models.Log{ IssueID: "", SessionID: sess.ID, WorkSessionID: wsID, Message: args[0], Type: logType, - }) + }); err != nil { + output.Warning("failed to add work-session log for %s: %v", wsID, err) + } // Get tagged issues for display issueIDs, _ := database.GetWorkSessionIssues(wsID) @@ -489,18 +505,23 @@ Flags support values, stdin (-), or file (@path): Uncertain: handoff.Uncertain, } - database.AddHandoff(issueHandoff) + if err := database.AddHandoff(issueHandoff); err != nil { + output.Warning("failed to record handoff for %s: %v", issueID, err) + continue + } // Capture git state gitState, _ := git.GetState() if gitState != nil { - database.AddGitSnapshot(&models.GitSnapshot{ + if err := database.AddGitSnapshot(&models.GitSnapshot{ IssueID: issueID, Event: "handoff", CommitSHA: gitState.CommitSHA, Branch: gitState.Branch, DirtyFiles: gitState.DirtyFiles, - }) + }); err != nil { + output.Warning("failed to capture git snapshot for %s: %v", issueID, err) + } } } @@ -517,8 +538,12 @@ Flags support values, stdin (-), or file (@path): ws.EndSHA = gitState.CommitSHA } - database.UpdateWorkSession(ws) - config.ClearActiveWorkSession(baseDir) + if err := database.UpdateWorkSession(ws); err != nil { + output.Warning("failed to update work session %s: %v", wsID, err) + } + if err := config.ClearActiveWorkSession(baseDir); err != nil { + output.Warning("failed to clear active work session %s: %v", wsID, err) + } } fmt.Printf("HANDOFF RECORDED %s\n", wsID) @@ -588,8 +613,12 @@ var wsEndCmd = &cobra.Command{ // End session now := time.Now() ws.EndedAt = &now - database.UpdateWorkSession(ws) - config.ClearActiveWorkSession(baseDir) + if err := database.UpdateWorkSession(ws); err != nil { + output.Warning("failed to update work session %s: %v", wsID, err) + } + if err := config.ClearActiveWorkSession(baseDir); err != nil { + output.Warning("failed to clear active work session %s: %v", wsID, err) + } output.Warning("No handoff recorded for %s", wsID) if len(issueIDs) > 0 { diff --git a/cmd/ws_test.go b/cmd/ws_test.go index 2ed9cdd8..aa0bbd05 100644 --- a/cmd/ws_test.go +++ b/cmd/ws_test.go @@ -23,9 +23,7 @@ func TestWsStartCreatesSession(t *testing.T) { StartSHA: "abc123", } - if err := database.CreateWorkSession(ws); err != nil { - t.Fatalf("CreateWorkSession failed: %v", err) - } + mustCreateWorkSession(t, database, ws) if ws.ID == "" { t.Error("Expected work session ID to be set") @@ -56,8 +54,8 @@ func TestWsStartWithActiveSessionErrors(t *testing.T) { // Create and set active session ws := &models.WorkSession{Name: "first-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) - config.SetActiveWorkSession(dir, ws.ID) + mustCreateWorkSession(t, database, ws) + mustSetActiveWorkSession(t, dir, ws.ID) // Check active session is set activeWS, err := config.GetActiveWorkSession(dir) @@ -83,11 +81,11 @@ func TestWsStopEndsSession(t *testing.T) { // Create active session ws := &models.WorkSession{Name: "active-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) - config.SetActiveWorkSession(dir, ws.ID) + mustCreateWorkSession(t, database, ws) + mustSetActiveWorkSession(t, dir, ws.ID) // End session - config.ClearActiveWorkSession(dir) + mustClearActiveWorkSession(t, dir) // Verify session is no longer active activeWS, _ := config.GetActiveWorkSession(dir) @@ -123,17 +121,15 @@ func TestWsTagAddsIssueToSession(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "tagging-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) - config.SetActiveWorkSession(dir, ws.ID) + mustCreateWorkSession(t, database, ws) + mustSetActiveWorkSession(t, dir, ws.ID) // Create issue issue := &models.Issue{Title: "Test Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Tag issue to work session - if err := database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session"); err != nil { - t.Fatalf("TagIssueToWorkSession failed: %v", err) - } + mustTagIssueToWorkSession(t, database, ws.ID, issue.ID, "test-session") // Verify issue is tagged issueIDs, err := database.GetWorkSessionIssues(ws.ID) @@ -159,7 +155,7 @@ func TestWsTagMultipleIssues(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "multi-tag-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + mustCreateWorkSession(t, database, ws) // Create issues issues := []*models.Issue{ @@ -169,8 +165,8 @@ func TestWsTagMultipleIssues(t *testing.T) { } for _, issue := range issues { - database.CreateIssue(issue) - database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session") + mustCreateIssue(t, database, issue) + mustTagIssueToWorkSession(t, database, ws.ID, issue.ID, "test-session") } // Verify all issues are tagged @@ -207,7 +203,7 @@ func TestWsTagInvalidIssueErrors(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "invalid-tag-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + mustCreateWorkSession(t, database, ws) // Try to get non-existent issue _, err = database.GetIssue("td-nonexistent") @@ -227,12 +223,12 @@ func TestWsUntagRemovesIssueFromSession(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "untag-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + mustCreateWorkSession(t, database, ws) // Create and tag issue issue := &models.Issue{Title: "Test Issue", Status: models.StatusOpen} - database.CreateIssue(issue) - database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session") + mustCreateIssue(t, database, issue) + mustTagIssueToWorkSession(t, database, ws.ID, issue.ID, "test-session") // Verify issue is tagged issueIDs, _ := database.GetWorkSessionIssues(ws.ID) @@ -241,9 +237,7 @@ func TestWsUntagRemovesIssueFromSession(t *testing.T) { } // Untag issue - if err := database.UntagIssueFromWorkSession(ws.ID, issue.ID, "test-session"); err != nil { - t.Fatalf("UntagIssueFromWorkSession failed: %v", err) - } + mustUntagIssueFromWorkSession(t, database, ws.ID, issue.ID, "test-session") // Verify issue is untagged issueIDs, _ = database.GetWorkSessionIssues(ws.ID) @@ -263,18 +257,18 @@ func TestWsUntagPartialRemoval(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "partial-untag-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + mustCreateWorkSession(t, database, ws) // Create and tag issues issue1 := &models.Issue{Title: "Issue 1", Status: models.StatusOpen} issue2 := &models.Issue{Title: "Issue 2", Status: models.StatusOpen} - database.CreateIssue(issue1) - database.CreateIssue(issue2) - database.TagIssueToWorkSession(ws.ID, issue1.ID, "test-session") - database.TagIssueToWorkSession(ws.ID, issue2.ID, "test-session") + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) + mustTagIssueToWorkSession(t, database, ws.ID, issue1.ID, "test-session") + mustTagIssueToWorkSession(t, database, ws.ID, issue2.ID, "test-session") // Untag only issue1 - database.UntagIssueFromWorkSession(ws.ID, issue1.ID, "test-session") + mustUntagIssueFromWorkSession(t, database, ws.ID, issue1.ID, "test-session") // Verify only issue2 remains issueIDs, _ := database.GetWorkSessionIssues(ws.ID) @@ -297,12 +291,12 @@ func TestWsLogAddsLogEntry(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "log-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + mustCreateWorkSession(t, database, ws) // Create and tag issue issue := &models.Issue{Title: "Test Issue", Status: models.StatusOpen} - database.CreateIssue(issue) - database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session") + mustCreateIssue(t, database, issue) + mustTagIssueToWorkSession(t, database, ws.ID, issue.ID, "test-session") // Add log to work session (log attached to work session, not specific issue) log := &models.Log{ @@ -312,9 +306,7 @@ func TestWsLogAddsLogEntry(t *testing.T) { Message: "Progress update", Type: models.LogTypeProgress, } - if err := database.AddLog(log); err != nil { - t.Fatalf("AddLog failed: %v", err) - } + mustAddLog(t, database, log) // Verify log was created with work session ID logs, err := database.GetLogsByWorkSession(ws.ID) @@ -340,7 +332,7 @@ func TestWsLogWithDifferentTypes(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "typed-log-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + mustCreateWorkSession(t, database, ws) testCases := []struct { logType models.LogType @@ -361,9 +353,7 @@ func TestWsLogWithDifferentTypes(t *testing.T) { Message: tc.message, Type: tc.logType, } - if err := database.AddLog(log); err != nil { - t.Fatalf("AddLog for %s failed: %v", tc.logType, err) - } + mustAddLog(t, database, log) } logs, _ := database.GetLogsByWorkSession(ws.ID) @@ -398,15 +388,15 @@ func TestWsHandoffCreatesHandoffs(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "handoff-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + mustCreateWorkSession(t, database, ws) // Create and tag issues issue1 := &models.Issue{Title: "Issue 1", Status: models.StatusInProgress} issue2 := &models.Issue{Title: "Issue 2", Status: models.StatusInProgress} - database.CreateIssue(issue1) - database.CreateIssue(issue2) - database.TagIssueToWorkSession(ws.ID, issue1.ID, "test-session") - database.TagIssueToWorkSession(ws.ID, issue2.ID, "test-session") + mustCreateIssue(t, database, issue1) + mustCreateIssue(t, database, issue2) + mustTagIssueToWorkSession(t, database, ws.ID, issue1.ID, "test-session") + mustTagIssueToWorkSession(t, database, ws.ID, issue2.ID, "test-session") // Create handoffs for each issue handoff1 := &models.Handoff{ @@ -467,8 +457,8 @@ func TestWsHandoffEndsSession(t *testing.T) { // Create and set active session ws := &models.WorkSession{Name: "ending-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) - config.SetActiveWorkSession(dir, ws.ID) + mustCreateWorkSession(t, database, ws) + mustSetActiveWorkSession(t, dir, ws.ID) // Verify session is active activeWS, _ := config.GetActiveWorkSession(dir) @@ -477,7 +467,7 @@ func TestWsHandoffEndsSession(t *testing.T) { } // Clear active session (simulates handoff ending session) - config.ClearActiveWorkSession(dir) + mustClearActiveWorkSession(t, dir) // Verify session is ended activeWS, _ = config.GetActiveWorkSession(dir) @@ -497,13 +487,13 @@ func TestWsCurrentShowsActiveSession(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "current-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) - config.SetActiveWorkSession(dir, ws.ID) + mustCreateWorkSession(t, database, ws) + mustSetActiveWorkSession(t, dir, ws.ID) // Tag issue issue := &models.Issue{Title: "Test Issue", Status: models.StatusInProgress} - database.CreateIssue(issue) - database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session") + mustCreateIssue(t, database, issue) + mustTagIssueToWorkSession(t, database, ws.ID, issue.ID, "test-session") // Get current session activeWS, _ := config.GetActiveWorkSession(dir) @@ -551,7 +541,7 @@ func TestWsListShowsSessions(t *testing.T) { sessions := []string{"session-1", "session-2", "session-3"} for _, name := range sessions { ws := &models.WorkSession{Name: name, SessionID: "ses_test"} - database.CreateWorkSession(ws) + mustCreateWorkSession(t, database, ws) } // List work sessions @@ -597,7 +587,7 @@ func TestWsListWithLimit(t *testing.T) { // Create 10 sessions for i := 0; i < 10; i++ { ws := &models.WorkSession{Name: "session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + mustCreateWorkSession(t, database, ws) } // List with limit 5 @@ -622,11 +612,11 @@ func TestWsEndWithoutHandoff(t *testing.T) { // Create and set active session ws := &models.WorkSession{Name: "no-handoff-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) - config.SetActiveWorkSession(dir, ws.ID) + mustCreateWorkSession(t, database, ws) + mustSetActiveWorkSession(t, dir, ws.ID) // End without handoff - config.ClearActiveWorkSession(dir) + mustClearActiveWorkSession(t, dir) // Verify session ended activeWS, _ := config.GetActiveWorkSession(dir) @@ -646,19 +636,19 @@ func TestWsTagAutoStartsOpenIssues(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "auto-start-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + mustCreateWorkSession(t, database, ws) // Create open issue issue := &models.Issue{Title: "Open Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Tag issue (simulating auto-start behavior) - database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session") + mustTagIssueToWorkSession(t, database, ws.ID, issue.ID, "test-session") // Simulate starting the issue issue.Status = models.StatusInProgress issue.ImplementerSession = "ses_test" - database.UpdateIssue(issue) + mustUpdateIssue(t, database, issue) // Verify issue is started retrieved, _ := database.GetIssue(issue.ID) @@ -678,14 +668,14 @@ func TestWsTagNoStartFlag(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "no-start-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + mustCreateWorkSession(t, database, ws) // Create open issue issue := &models.Issue{Title: "Open Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + mustCreateIssue(t, database, issue) // Tag issue without starting (simulating --no-start) - database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session") + mustTagIssueToWorkSession(t, database, ws.ID, issue.ID, "test-session") // Issue should remain open (with --no-start) retrieved, _ := database.GetIssue(issue.ID) @@ -705,12 +695,12 @@ func TestWsShowDisplaysPastSession(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "past-session", SessionID: "ses_test", StartSHA: "abc123"} - database.CreateWorkSession(ws) + mustCreateWorkSession(t, database, ws) // Tag issue issue := &models.Issue{Title: "Test Issue", Status: models.StatusInProgress} - database.CreateIssue(issue) - database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session") + mustCreateIssue(t, database, issue) + mustTagIssueToWorkSession(t, database, ws.ID, issue.ID, "test-session") // Get session details retrieved, err := database.GetWorkSession(ws.ID) @@ -759,7 +749,7 @@ func TestWsHandoffAutoPopulatesFromLogs(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "auto-populate-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + mustCreateWorkSession(t, database, ws) // Add various log types logs := []models.Log{ @@ -770,7 +760,7 @@ func TestWsHandoffAutoPopulatesFromLogs(t *testing.T) { } for _, log := range logs { - database.AddLog(&log) + mustAddLog(t, database, &log) } // Get logs for session @@ -807,7 +797,7 @@ func TestWsUpdateSession(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "update-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + mustCreateWorkSession(t, database, ws) // Update session with end SHA ws.EndSHA = "def456" diff --git a/internal/agent/instructions_test.go b/internal/agent/instructions_test.go index cdfd35b7..3fdcd7f5 100644 --- a/internal/agent/instructions_test.go +++ b/internal/agent/instructions_test.go @@ -6,6 +6,13 @@ import ( "testing" ) +func mustWriteFile(t *testing.T, path, content string) { + t.Helper() + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatalf("WriteFile(%s): %v", path, err) + } +} + func TestKnownAgentFiles(t *testing.T) { expected := []string{ "AGENTS.md", @@ -31,8 +38,8 @@ func TestKnownAgentFiles(t *testing.T) { func TestDetectAgentFile(t *testing.T) { t.Run("finds AGENTS.md first", func(t *testing.T) { dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "AGENTS.md"), []byte("# Agents"), 0644) - os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("# Claude"), 0644) + mustWriteFile(t, filepath.Join(dir, "AGENTS.md"), "# Agents") + mustWriteFile(t, filepath.Join(dir, "CLAUDE.md"), "# Claude") got := DetectAgentFile(dir) if filepath.Base(got) != "AGENTS.md" { @@ -42,7 +49,7 @@ func TestDetectAgentFile(t *testing.T) { t.Run("finds GEMINI.md", func(t *testing.T) { dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "GEMINI.md"), []byte("# Gemini"), 0644) + mustWriteFile(t, filepath.Join(dir, "GEMINI.md"), "# Gemini") got := DetectAgentFile(dir) if filepath.Base(got) != "GEMINI.md" { @@ -52,7 +59,7 @@ func TestDetectAgentFile(t *testing.T) { t.Run("finds CLAUDE.local.md", func(t *testing.T) { dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "CLAUDE.local.md"), []byte("# Local"), 0644) + mustWriteFile(t, filepath.Join(dir, "CLAUDE.local.md"), "# Local") got := DetectAgentFile(dir) if filepath.Base(got) != "CLAUDE.local.md" { @@ -62,7 +69,7 @@ func TestDetectAgentFile(t *testing.T) { t.Run("finds CODEX.md", func(t *testing.T) { dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "CODEX.md"), []byte("# Codex"), 0644) + mustWriteFile(t, filepath.Join(dir, "CODEX.md"), "# Codex") got := DetectAgentFile(dir) if filepath.Base(got) != "CODEX.md" { @@ -83,8 +90,8 @@ func TestDetectAgentFile(t *testing.T) { func TestPreferredAgentFile(t *testing.T) { t.Run("prefers AGENTS.md when it exists", func(t *testing.T) { dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "AGENTS.md"), []byte("# Agents"), 0644) - os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("# Claude"), 0644) + mustWriteFile(t, filepath.Join(dir, "AGENTS.md"), "# Agents") + mustWriteFile(t, filepath.Join(dir, "CLAUDE.md"), "# Claude") got := PreferredAgentFile(dir) if filepath.Base(got) != "AGENTS.md" { @@ -94,7 +101,7 @@ func TestPreferredAgentFile(t *testing.T) { t.Run("uses CLAUDE.md when AGENTS.md missing", func(t *testing.T) { dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("# Claude"), 0644) + mustWriteFile(t, filepath.Join(dir, "CLAUDE.md"), "# Claude") got := PreferredAgentFile(dir) if filepath.Base(got) != "CLAUDE.md" { @@ -104,7 +111,7 @@ func TestPreferredAgentFile(t *testing.T) { t.Run("uses GEMINI.md when AGENTS.md and CLAUDE.md missing", func(t *testing.T) { dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "GEMINI.md"), []byte("# Gemini"), 0644) + mustWriteFile(t, filepath.Join(dir, "GEMINI.md"), "# Gemini") got := PreferredAgentFile(dir) if filepath.Base(got) != "GEMINI.md" { @@ -114,7 +121,7 @@ func TestPreferredAgentFile(t *testing.T) { t.Run("uses CODEX.md when higher-priority files missing", func(t *testing.T) { dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "CODEX.md"), []byte("# Codex"), 0644) + mustWriteFile(t, filepath.Join(dir, "CODEX.md"), "# Codex") got := PreferredAgentFile(dir) if filepath.Base(got) != "CODEX.md" { @@ -136,7 +143,7 @@ func TestHasTDInstructions(t *testing.T) { t.Run("returns true when file contains td usage", func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "CLAUDE.md") - os.WriteFile(path, []byte("Run td usage --new-session"), 0644) + mustWriteFile(t, path, "Run td usage --new-session") if !HasTDInstructions(path) { t.Error("HasTDInstructions = false, want true") @@ -146,7 +153,7 @@ func TestHasTDInstructions(t *testing.T) { t.Run("returns false when file has no td usage", func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "CLAUDE.md") - os.WriteFile(path, []byte("# Claude instructions"), 0644) + mustWriteFile(t, path, "# Claude instructions") if HasTDInstructions(path) { t.Error("HasTDInstructions = true, want false") @@ -163,7 +170,7 @@ func TestHasTDInstructions(t *testing.T) { func TestAnyFileHasTDInstructions(t *testing.T) { t.Run("returns true when CLAUDE.md has instructions", func(t *testing.T) { dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("Run td usage --new-session"), 0644) + mustWriteFile(t, filepath.Join(dir, "CLAUDE.md"), "Run td usage --new-session") if !AnyFileHasTDInstructions(dir) { t.Error("AnyFileHasTDInstructions = false, want true") @@ -172,7 +179,7 @@ func TestAnyFileHasTDInstructions(t *testing.T) { t.Run("returns true when GEMINI.md has instructions", func(t *testing.T) { dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "GEMINI.md"), []byte("Use td usage -q"), 0644) + mustWriteFile(t, filepath.Join(dir, "GEMINI.md"), "Use td usage -q") if !AnyFileHasTDInstructions(dir) { t.Error("AnyFileHasTDInstructions = false, want true") @@ -181,7 +188,7 @@ func TestAnyFileHasTDInstructions(t *testing.T) { t.Run("returns true when CLAUDE.local.md has instructions", func(t *testing.T) { dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "CLAUDE.local.md"), []byte("td usage"), 0644) + mustWriteFile(t, filepath.Join(dir, "CLAUDE.local.md"), "td usage") if !AnyFileHasTDInstructions(dir) { t.Error("AnyFileHasTDInstructions = false, want true") @@ -190,7 +197,7 @@ func TestAnyFileHasTDInstructions(t *testing.T) { t.Run("returns true when CODEX.md has instructions", func(t *testing.T) { dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "CODEX.md"), []byte("td usage --new-session"), 0644) + mustWriteFile(t, filepath.Join(dir, "CODEX.md"), "td usage --new-session") if !AnyFileHasTDInstructions(dir) { t.Error("AnyFileHasTDInstructions = false, want true") @@ -199,8 +206,8 @@ func TestAnyFileHasTDInstructions(t *testing.T) { t.Run("returns false when files exist but no instructions", func(t *testing.T) { dir := t.TempDir() - os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("# Claude"), 0644) - os.WriteFile(filepath.Join(dir, "GEMINI.md"), []byte("# Gemini"), 0644) + mustWriteFile(t, filepath.Join(dir, "CLAUDE.md"), "# Claude") + mustWriteFile(t, filepath.Join(dir, "GEMINI.md"), "# Gemini") if AnyFileHasTDInstructions(dir) { t.Error("AnyFileHasTDInstructions = true, want false") @@ -218,9 +225,9 @@ func TestAnyFileHasTDInstructions(t *testing.T) { t.Run("finds instructions in non-primary file", func(t *testing.T) { dir := t.TempDir() // CLAUDE.md exists but has no instructions - os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("# Claude"), 0644) + mustWriteFile(t, filepath.Join(dir, "CLAUDE.md"), "# Claude") // GEMINI.local.md has instructions - os.WriteFile(filepath.Join(dir, "GEMINI.local.md"), []byte("td usage"), 0644) + mustWriteFile(t, filepath.Join(dir, "GEMINI.local.md"), "td usage") if !AnyFileHasTDInstructions(dir) { t.Error("AnyFileHasTDInstructions = false, want true (found in GEMINI.local.md)") diff --git a/internal/serve/portfile_unix.go b/internal/serve/portfile_unix.go index 26a3b55f..8bcb3ee8 100644 --- a/internal/serve/portfile_unix.go +++ b/internal/serve/portfile_unix.go @@ -37,7 +37,7 @@ func acquireFileLockTimeout(f *os.File, timeout time.Duration) error { // releaseFileLock releases the exclusive flock. func releaseFileLock(f *os.File) { if f != nil { - syscall.Flock(int(f.Fd()), syscall.LOCK_UN) + _ = syscall.Flock(int(f.Fd()), syscall.LOCK_UN) } } diff --git a/internal/serve/response_test.go b/internal/serve/response_test.go index a624abbf..c80a2da6 100644 --- a/internal/serve/response_test.go +++ b/internal/serve/response_test.go @@ -127,7 +127,9 @@ func TestEnvelopeJSONShape(t *testing.T) { WriteSuccess(w, "hello", http.StatusOK) var raw map[string]interface{} - json.Unmarshal(w.Body.Bytes(), &raw) + if err := json.Unmarshal(w.Body.Bytes(), &raw); err != nil { + t.Fatalf("unmarshal: %v", err) + } if _, exists := raw["error"]; exists { t.Error("success response should not have 'error' key") @@ -145,7 +147,9 @@ func TestEnvelopeJSONShape(t *testing.T) { WriteError(w, ErrInternal, "fail", http.StatusInternalServerError) var raw map[string]interface{} - json.Unmarshal(w.Body.Bytes(), &raw) + if err := json.Unmarshal(w.Body.Bytes(), &raw); err != nil { + t.Fatalf("unmarshal: %v", err) + } if _, exists := raw["data"]; exists { t.Error("error response should not have 'data' key") @@ -329,7 +333,9 @@ func TestIssueToDTO_LabelsNeverNull(t *testing.T) { } var raw map[string]interface{} - json.Unmarshal(data, &raw) + if err := json.Unmarshal(data, &raw); err != nil { + t.Fatalf("unmarshal: %v", err) + } labels, ok := raw["labels"].([]interface{}) if !ok { t.Fatalf("labels should be array, got %T (%v)", raw["labels"], raw["labels"]) @@ -357,7 +363,9 @@ func TestIssueToDTO_NullableFieldsSerializeAsNull(t *testing.T) { } var raw map[string]interface{} - json.Unmarshal(data, &raw) + if err := json.Unmarshal(data, &raw); err != nil { + t.Fatalf("unmarshal: %v", err) + } nullFields := []string{"parent_id", "implementer_session", "creator_session", "reviewer_session", "created_branch", "defer_until", "due_date", "closed_at", "deleted_at"} @@ -472,7 +480,9 @@ func TestHandoffToDTO_EmptySlicesNotNull(t *testing.T) { // Verify JSON data, _ := json.Marshal(dto) var raw map[string]interface{} - json.Unmarshal(data, &raw) + if err := json.Unmarshal(data, &raw); err != nil { + t.Fatalf("unmarshal: %v", err) + } for _, field := range []string{"done", "remaining", "decisions", "uncertain"} { arr, ok := raw[field].([]interface{}) diff --git a/internal/serve/sse.go b/internal/serve/sse.go index 6d0aa399..4f227b88 100644 --- a/internal/serve/sse.go +++ b/internal/serve/sse.go @@ -397,7 +397,7 @@ func serveAutoSyncPush(database *db.DB, client *syncclient.Client, state *db.Syn if err != nil { return fmt.Errorf("begin tx: %w", err) } - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() events, err := tdsync.GetPendingEvents(tx, deviceID, sessionID) if err != nil { @@ -502,12 +502,12 @@ func serveAutoSyncPull(database *db.DB, client *syncclient.Client, state *db.Syn // Accept all entity types in SSE path (no feature gating for live sync) allowAll := func(string) bool { return true } if _, err := tdsync.ApplyRemoteEvents(tx, events, deviceID, allowAll, state.LastSyncAt); err != nil { - tx.Rollback() + _ = tx.Rollback() return fmt.Errorf("apply events: %w", err) } if _, err := tx.Exec(`UPDATE sync_state SET last_pulled_server_seq = ?, last_sync_at = CURRENT_TIMESTAMP`, pullResp.LastServerSeq); err != nil { - tx.Rollback() + _ = tx.Rollback() return fmt.Errorf("update sync state: %w", err) } diff --git a/internal/sync/backfill_test.go b/internal/sync/backfill_test.go index 758abf49..37c8d969 100644 --- a/internal/sync/backfill_test.go +++ b/internal/sync/backfill_test.go @@ -249,14 +249,18 @@ func TestBackfillStaleIssues_AddsUpdate(t *testing.T) { if err != nil { t.Fatal(err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatal(err) + } if n != 1 { t.Fatalf("expected 1 stale backfill, got %d", n) } var count int - db.QueryRow(`SELECT COUNT(*) FROM action_log WHERE entity_id='td-700' AND action_type='create'`).Scan(&count) + if err := db.QueryRow(`SELECT COUNT(*) FROM action_log WHERE entity_id='td-700' AND action_type='create'`).Scan(&count); err != nil { + t.Fatal(err) + } if count != 2 { t.Fatalf("expected 2 create entries for td-700 (original + backfill), got %d", count) } @@ -275,7 +279,9 @@ func TestBackfillStaleIssues_SkipsWhenUpToDate(t *testing.T) { if err != nil { t.Fatal(err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatal(err) + } if n != 0 { t.Fatalf("expected 0 stale updates, got %d", n) @@ -295,7 +301,9 @@ func TestBackfillStaleIssues_BackfillsInvalidJSON(t *testing.T) { if err != nil { t.Fatal(err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatal(err) + } if n != 1 { t.Fatalf("expected 1 stale update for invalid JSON, got %d", n) @@ -314,7 +322,9 @@ func TestBackfillOrphanEntities_MultipleEntityTypes(t *testing.T) { if err != nil { t.Fatal(err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatal(err) + } if n != 3 { t.Fatalf("expected 3 backfilled, got %d", n) @@ -400,7 +410,9 @@ func TestBackfillOrphanEntities_IncludesSoftDeleted(t *testing.T) { if err != nil { t.Fatal(err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatal(err) + } if n != 1 { t.Fatalf("expected 1 backfilled (soft-deleted), got %d", n) @@ -430,7 +442,9 @@ func TestBackfillOrphanEntities_SkipsAfterPull(t *testing.T) { if err != nil { t.Fatal(err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatal(err) + } if n != 0 { t.Fatalf("expected 0 backfilled after pull, got %d", n) @@ -438,7 +452,9 @@ func TestBackfillOrphanEntities_SkipsAfterPull(t *testing.T) { // Verify no action_log entries were created var count int - db.QueryRow(`SELECT COUNT(*) FROM action_log`).Scan(&count) + if err := db.QueryRow(`SELECT COUNT(*) FROM action_log`).Scan(&count); err != nil { + t.Fatal(err) + } if count != 0 { t.Fatalf("expected 0 action_log rows, got %d", count) } diff --git a/internal/sync/client_test.go b/internal/sync/client_test.go index 5d1b8a3c..f763fa34 100644 --- a/internal/sync/client_test.go +++ b/internal/sync/client_test.go @@ -69,6 +69,13 @@ func insertActionLog(t *testing.T, db *sql.DB, id, sessionID, actionType, entity } } +func mustCommitTx(t *testing.T, tx *sql.Tx) { + t.Helper() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } +} + func TestGetPendingEvents_Basic(t *testing.T) { db := setupClientDB(t) @@ -83,7 +90,7 @@ func TestGetPendingEvents_Basic(t *testing.T) { if err != nil { t.Fatalf("begin: %v", err) } - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() events, err := GetPendingEvents(tx, "device1", "sync-sess") if err != nil { @@ -153,7 +160,7 @@ func TestGetPendingEvents_SkipsUndone(t *testing.T) { `{"title":"Also keep"}`, `{"title":"Keep"}`, 0, "") tx, _ := db.Begin() - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() events, err := GetPendingEvents(tx, "d1", "s1") if err != nil { @@ -176,7 +183,7 @@ func TestGetPendingEvents_SkipsSynced(t *testing.T) { `{"title":"Pending"}`, `{}`, 0, "") tx, _ := db.Begin() - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() events, err := GetPendingEvents(tx, "d1", "s1") if err != nil { @@ -225,7 +232,7 @@ func TestGetPendingEvents_ActionTypeMapping(t *testing.T) { } tx, _ := db.Begin() - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() events, err := GetPendingEvents(tx, "d1", "s1") if err != nil { @@ -260,7 +267,7 @@ func TestGetPendingEvents_EntityTypeNormalization(t *testing.T) { `{"foo":"bar"}`, `{}`, 0, "") tx, _ := db.Begin() - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() events, err := GetPendingEvents(tx, "d1", "s1") if err != nil { @@ -318,7 +325,7 @@ func TestApplyRemoteEvents_Basic(t *testing.T) { if err != nil { t.Fatalf("ApplyRemoteEvents: %v", err) } - tx.Commit() + mustCommitTx(t, tx) if result.Applied != 3 { t.Fatalf("Applied: got %d, want 3", result.Applied) @@ -379,7 +386,7 @@ func TestApplyRemoteEvents_PartialFailure(t *testing.T) { if err != nil { t.Fatalf("ApplyRemoteEvents: %v", err) } - tx.Commit() + mustCommitTx(t, tx) if result.Applied != 2 { t.Fatalf("Applied: got %d, want 2", result.Applied) @@ -396,7 +403,9 @@ func TestApplyRemoteEvents_PartialFailure(t *testing.T) { // Verify good entities exist var count int - db.QueryRow("SELECT COUNT(*) FROM issues").Scan(&count) + if err := db.QueryRow("SELECT COUNT(*) FROM issues").Scan(&count); err != nil { + t.Fatalf("count issues: %v", err) + } if count != 2 { t.Fatalf("issues count: got %d, want 2", count) } @@ -411,7 +420,7 @@ func TestApplyRemoteEvents_ConflictTracking(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p1); err != nil { t.Fatalf("seed: %v", err) } - tx.Commit() + mustCommitTx(t, tx) // Apply remote event that overwrites remotePayload, _ := json.Marshal(map[string]any{ @@ -432,7 +441,7 @@ func TestApplyRemoteEvents_ConflictTracking(t *testing.T) { if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() + mustCommitTx(t, tx) if result.Overwrites != 1 { t.Fatalf("expected 1 overwrite, got %d", result.Overwrites) @@ -479,7 +488,7 @@ func TestApplyRemoteEvents_MultipleOverwritesProduceConflicts(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i2", p2); err != nil { t.Fatalf("seed i2: %v", err) } - tx.Commit() + mustCommitTx(t, tx) // Apply batch of remote events that overwrite both makePayload := func(title, status string) []byte { @@ -500,7 +509,7 @@ func TestApplyRemoteEvents_MultipleOverwritesProduceConflicts(t *testing.T) { if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() + mustCommitTx(t, tx) if result.Applied != 2 { t.Fatalf("Applied=%d, want 2", result.Applied) @@ -534,7 +543,7 @@ func TestApplyRemoteEvents_DeleteDoesNotProduceConflict(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p); err != nil { t.Fatalf("seed: %v", err) } - tx.Commit() + mustCommitTx(t, tx) // Apply a delete event from remote deletePayload, _ := json.Marshal(map[string]any{ @@ -550,7 +559,7 @@ func TestApplyRemoteEvents_DeleteDoesNotProduceConflict(t *testing.T) { if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() + mustCommitTx(t, tx) if result.Applied != 1 { t.Fatalf("Applied=%d, want 1", result.Applied) @@ -564,7 +573,9 @@ func TestApplyRemoteEvents_DeleteDoesNotProduceConflict(t *testing.T) { // Verify row is actually deleted var count int - db.QueryRow("SELECT COUNT(*) FROM issues WHERE id = ?", "i1").Scan(&count) + if err := db.QueryRow("SELECT COUNT(*) FROM issues WHERE id = ?", "i1").Scan(&count); err != nil { + t.Fatalf("count deleted row: %v", err) + } if count != 0 { t.Fatal("row should be deleted") } @@ -583,7 +594,7 @@ func TestApplyRemoteEvents_ConflictDataCorrectness(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", localFields); err != nil { t.Fatalf("seed: %v", err) } - tx.Commit() + mustCommitTx(t, tx) // Remote overwrites with different data remoteFields := map[string]any{ @@ -605,7 +616,7 @@ func TestApplyRemoteEvents_ConflictDataCorrectness(t *testing.T) { if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() + mustCommitTx(t, tx) if len(result.Conflicts) != 1 { t.Fatalf("expected 1 conflict, got %d", len(result.Conflicts)) @@ -660,7 +671,7 @@ func TestApplyRemoteEvents_NoConflictWhenUnchangedSinceSync(t *testing.T) { if err != nil { t.Fatalf("seed: %v", err) } - tx.Commit() + mustCommitTx(t, tx) // lastSyncAt is AFTER the local row's updated_at → no conflict expected syncTime := time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC) @@ -679,7 +690,7 @@ func TestApplyRemoteEvents_NoConflictWhenUnchangedSinceSync(t *testing.T) { if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() + mustCommitTx(t, tx) if result.Applied != 1 { t.Fatalf("Applied=%d, want 1", result.Applied) @@ -703,7 +714,7 @@ func TestApplyRemoteEvents_ConflictWhenModifiedAfterSync(t *testing.T) { if err != nil { t.Fatalf("seed: %v", err) } - tx.Commit() + mustCommitTx(t, tx) // lastSyncAt is BEFORE the local row's updated_at → conflict expected syncTime := time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC) @@ -722,7 +733,7 @@ func TestApplyRemoteEvents_ConflictWhenModifiedAfterSync(t *testing.T) { if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() + mustCommitTx(t, tx) if result.Overwrites != 1 { t.Fatalf("Overwrites=%d, want 1 (local was modified after sync)", result.Overwrites) @@ -741,7 +752,7 @@ func TestApplyRemoteEvents_NilLastSyncAtSkipsConflicts(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p); err != nil { t.Fatalf("seed: %v", err) } - tx.Commit() + mustCommitTx(t, tx) // Apply remote overwrite with nil lastSyncAt (bootstrap scenario) remotePayload, _ := json.Marshal(map[string]any{ @@ -758,7 +769,7 @@ func TestApplyRemoteEvents_NilLastSyncAtSkipsConflicts(t *testing.T) { if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() + mustCommitTx(t, tx) if result.Overwrites != 0 { t.Fatalf("Overwrites=%d, want 0 (nil lastSyncAt = no conflicts)", result.Overwrites) @@ -777,8 +788,12 @@ func TestMarkEventsSynced(t *testing.T) { // Get rowids for first two rows var rowid1, rowid2 int64 - db.QueryRow("SELECT rowid FROM action_log WHERE id = ?", "al-00000001").Scan(&rowid1) - db.QueryRow("SELECT rowid FROM action_log WHERE id = ?", "al-00000002").Scan(&rowid2) + if err := db.QueryRow("SELECT rowid FROM action_log WHERE id = ?", "al-00000001").Scan(&rowid1); err != nil { + t.Fatalf("rowid1: %v", err) + } + if err := db.QueryRow("SELECT rowid FROM action_log WHERE id = ?", "al-00000002").Scan(&rowid2); err != nil { + t.Fatalf("rowid2: %v", err) + } acks := []Ack{ {ClientActionID: rowid1, ServerSeq: 100}, @@ -789,13 +804,15 @@ func TestMarkEventsSynced(t *testing.T) { if err := MarkEventsSynced(tx, acks); err != nil { t.Fatalf("MarkEventsSynced: %v", err) } - tx.Commit() + mustCommitTx(t, tx) // Verify synced rows var syncedAt sql.NullString var serverSeq sql.NullInt64 - db.QueryRow("SELECT synced_at, server_seq FROM action_log WHERE id = ?", "al-00000001").Scan(&syncedAt, &serverSeq) + if err := db.QueryRow("SELECT synced_at, server_seq FROM action_log WHERE id = ?", "al-00000001").Scan(&syncedAt, &serverSeq); err != nil { + t.Fatalf("al-00000001: %v", err) + } if !syncedAt.Valid { t.Error("al-00000001: synced_at should be set") } @@ -803,7 +820,9 @@ func TestMarkEventsSynced(t *testing.T) { t.Errorf("al-00000001: server_seq got %v, want 100", serverSeq) } - db.QueryRow("SELECT synced_at, server_seq FROM action_log WHERE id = ?", "al-00000002").Scan(&syncedAt, &serverSeq) + if err := db.QueryRow("SELECT synced_at, server_seq FROM action_log WHERE id = ?", "al-00000002").Scan(&syncedAt, &serverSeq); err != nil { + t.Fatalf("al-00000002: %v", err) + } if !syncedAt.Valid { t.Error("al-00000002: synced_at should be set") } @@ -812,7 +831,9 @@ func TestMarkEventsSynced(t *testing.T) { } // Verify unsynced row - db.QueryRow("SELECT synced_at, server_seq FROM action_log WHERE id = ?", "al-00000003").Scan(&syncedAt, &serverSeq) + if err := db.QueryRow("SELECT synced_at, server_seq FROM action_log WHERE id = ?", "al-00000003").Scan(&syncedAt, &serverSeq); err != nil { + t.Fatalf("al-00000003: %v", err) + } if syncedAt.Valid { t.Error("al-00000003: synced_at should NOT be set") } @@ -822,7 +843,7 @@ func TestMarkEventsSynced(t *testing.T) { // Verify GetPendingEvents now only returns the unsynced one tx, _ = db.Begin() - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() events, err := GetPendingEvents(tx, "d1", "s1") if err != nil { t.Fatalf("GetPendingEvents: %v", err) @@ -853,7 +874,7 @@ func TestGetPendingEvents_NullID(t *testing.T) { if err != nil { t.Fatalf("begin: %v", err) } - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() events, err := GetPendingEvents(tx, "device1", "sync-sess") if err != nil { @@ -890,7 +911,7 @@ func TestGetPendingEvents_RealActionTypesIntegration(t *testing.T) { if err != nil { t.Fatalf("begin: %v", err) } - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() events, err := GetPendingEvents(tx, "device-int", "sess-int") if err != nil { diff --git a/internal/sync/engine_test.go b/internal/sync/engine_test.go index afe65981..a05ec5db 100644 --- a/internal/sync/engine_test.go +++ b/internal/sync/engine_test.go @@ -48,7 +48,9 @@ func TestInsertServerEvents_Basic(t *testing.T) { if err != nil { t.Fatalf("insert: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } if result.Accepted != 3 { t.Fatalf("accepted: got %d, want 3", result.Accepted) @@ -90,7 +92,9 @@ func TestInsertServerEvents_Dedup(t *testing.T) { if err != nil { t.Fatalf("first insert: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } if r1.Accepted != 3 { t.Fatalf("first: accepted=%d, want 3", r1.Accepted) @@ -102,7 +106,9 @@ func TestInsertServerEvents_Dedup(t *testing.T) { if err != nil { t.Fatalf("second insert: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } if r2.Accepted != 0 { t.Fatalf("second: accepted=%d, want 0", r2.Accepted) @@ -122,7 +128,9 @@ func TestInsertServerEvents_Dedup(t *testing.T) { // Verify total count in DB var count int - db.QueryRow("SELECT COUNT(*) FROM events").Scan(&count) + if err := db.QueryRow("SELECT COUNT(*) FROM events").Scan(&count); err != nil { + t.Fatalf("count events: %v", err) + } if count != 3 { t.Fatalf("total events: got %d, want 3", count) } @@ -149,7 +157,9 @@ func TestInsertServerEvents_ValidationReject(t *testing.T) { if err != nil { t.Fatalf("insert: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } if result.Accepted != 0 { t.Fatalf("accepted: got %d, want 0", result.Accepted) @@ -185,14 +195,18 @@ func TestGetEventsSince_All(t *testing.T) { if _, err := InsertServerEvents(tx, events); err != nil { t.Fatalf("insert: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } tx, _ = db.Begin() result, err := GetEventsSince(tx, 0, 100, "") if err != nil { t.Fatalf("get: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } if len(result.Events) != 5 { t.Fatalf("events: got %d, want 5", len(result.Events)) @@ -216,14 +230,18 @@ func TestGetEventsSince_Partial(t *testing.T) { if _, err := InsertServerEvents(tx, events); err != nil { t.Fatalf("insert: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } tx, _ = db.Begin() result, err := GetEventsSince(tx, 3, 100, "") if err != nil { t.Fatalf("get: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } if len(result.Events) != 2 { t.Fatalf("events: got %d, want 2", len(result.Events)) @@ -247,14 +265,18 @@ func TestGetEventsSince_Limit(t *testing.T) { if _, err := InsertServerEvents(tx, events); err != nil { t.Fatalf("insert: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } tx, _ = db.Begin() result, err := GetEventsSince(tx, 0, 3, "") if err != nil { t.Fatalf("get: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } if len(result.Events) != 3 { t.Fatalf("events: got %d, want 3", len(result.Events)) @@ -277,14 +299,18 @@ func TestGetEventsSince_ExcludeDevice(t *testing.T) { if _, err := InsertServerEvents(tx, events); err != nil { t.Fatalf("insert: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } tx, _ = db.Begin() result, err := GetEventsSince(tx, 0, 100, "d1") if err != nil { t.Fatalf("get: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } if len(result.Events) != 2 { t.Fatalf("events: got %d, want 2", len(result.Events)) @@ -304,7 +330,9 @@ func TestGetEventsSince_Empty(t *testing.T) { if err != nil { t.Fatalf("get: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } if len(result.Events) != 0 { t.Fatalf("events: got %d, want 0", len(result.Events)) diff --git a/internal/sync/events_test.go b/internal/sync/events_test.go index 2df11d5f..5ad00b9c 100644 --- a/internal/sync/events_test.go +++ b/internal/sync/events_test.go @@ -66,7 +66,9 @@ func TestUpsertEntity_Create(t *testing.T) { if err != nil { t.Fatalf("upsert: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } var title, status string err = db.QueryRow("SELECT title, status FROM issues WHERE id = ?", "i1").Scan(&title, &status) @@ -87,7 +89,9 @@ func TestUpsertEntity_Update(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p1); err != nil { t.Fatalf("insert: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } // Upsert with new title tx = beginTx(t, db) @@ -95,10 +99,14 @@ func TestUpsertEntity_Update(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p2); err != nil { t.Fatalf("upsert: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } var title, status string - db.QueryRow("SELECT title, status FROM issues WHERE id = ?", "i1").Scan(&title, &status) + if err := db.QueryRow("SELECT title, status FROM issues WHERE id = ?", "i1").Scan(&title, &status); err != nil { + t.Fatalf("query issue: %v", err) + } if title != "new" || status != "closed" { t.Fatalf("got title=%q status=%q", title, status) } @@ -113,7 +121,9 @@ func TestUpsertExistingEntity(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p1); err != nil { t.Fatalf("create: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } // Upsert with completely different data tx = beginTx(t, db) @@ -121,12 +131,16 @@ func TestUpsertExistingEntity(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p2); err != nil { t.Fatalf("upsert: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } var title string var priority sql.NullString var status sql.NullString - db.QueryRow("SELECT title, status, priority FROM issues WHERE id = ?", "i1").Scan(&title, &status, &priority) + if err := db.QueryRow("SELECT title, status, priority FROM issues WHERE id = ?", "i1").Scan(&title, &status, &priority); err != nil { + t.Fatalf("query issue: %v", err) + } if title != "replaced" { t.Fatalf("title should be replaced, got %q", title) } @@ -148,7 +162,9 @@ func TestPartialPayloadDropsColumns(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p1); err != nil { t.Fatalf("create: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } // Upsert with only title tx = beginTx(t, db) @@ -156,11 +172,15 @@ func TestPartialPayloadDropsColumns(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p2); err != nil { t.Fatalf("upsert: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } var title string var status, priority sql.NullString - db.QueryRow("SELECT title, status, priority FROM issues WHERE id = ?", "i1").Scan(&title, &status, &priority) + if err := db.QueryRow("SELECT title, status, priority FROM issues WHERE id = ?", "i1").Scan(&title, &status, &priority); err != nil { + t.Fatalf("query issue: %v", err) + } if title != "partial" { t.Fatalf("title should be partial, got %q", title) } @@ -175,7 +195,7 @@ func TestPartialPayloadDropsColumns(t *testing.T) { func TestNilPayload(t *testing.T) { db := setupDB(t) tx := beginTx(t, db) - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() _, err := ApplyEvent(tx, Event{ ActionType: "create", @@ -191,7 +211,7 @@ func TestNilPayload(t *testing.T) { func TestEmptyEntityID(t *testing.T) { db := setupDB(t) tx := beginTx(t, db) - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() _, err := ApplyEvent(tx, Event{ ActionType: "create", @@ -207,7 +227,7 @@ func TestEmptyEntityID(t *testing.T) { func TestMalformedJSON(t *testing.T) { db := setupDB(t) tx := beginTx(t, db) - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() _, err := ApplyEvent(tx, Event{ ActionType: "create", @@ -234,7 +254,9 @@ func TestUpdateDoesNotRecreateAfterDelete(t *testing.T) { if err != nil { t.Fatalf("create: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } // Delete tx = beginTx(t, db) @@ -247,7 +269,9 @@ func TestUpdateDoesNotRecreateAfterDelete(t *testing.T) { if err != nil { t.Fatalf("delete: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } // Update after delete should be ignored tx = beginTx(t, db) @@ -260,10 +284,14 @@ func TestUpdateDoesNotRecreateAfterDelete(t *testing.T) { if err != nil { t.Fatalf("update: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } var count int - db.QueryRow("SELECT COUNT(*) FROM issues WHERE id = ?", "i1").Scan(&count) + if err := db.QueryRow("SELECT COUNT(*) FROM issues WHERE id = ?", "i1").Scan(&count); err != nil { + t.Fatalf("count issue: %v", err) + } if count != 0 { t.Fatalf("expected issue to remain deleted, got count=%d", count) } @@ -272,7 +300,7 @@ func TestUpdateDoesNotRecreateAfterDelete(t *testing.T) { func TestColumnNameInjection_DroppedSilently(t *testing.T) { db := setupDB(t) tx := beginTx(t, db) - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() // Injection column name is not a valid table column, so it gets silently dropped. // With no known fields remaining, the upsert returns an error — no injection occurs. @@ -288,7 +316,9 @@ func TestColumnNameInjection_DroppedSilently(t *testing.T) { // Verify the table wasn't dropped var count int - db.QueryRow("SELECT COUNT(*) FROM issues").Scan(&count) + if err := db.QueryRow("SELECT COUNT(*) FROM issues").Scan(&count); err != nil { + t.Fatalf("count issues: %v", err) + } if count != 0 { t.Fatalf("expected 0 rows, got %d", count) } @@ -299,16 +329,22 @@ func TestDeleteEntity(t *testing.T) { tx := beginTx(t, db) p, _ := json.Marshal(map[string]any{"title": "bye"}) _, _ = upsertEntity(tx, "issues", "i1", p) - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } tx = beginTx(t, db) if err := deleteEntity(tx, "issues", "i1"); err != nil { t.Fatalf("delete: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } var count int - db.QueryRow("SELECT COUNT(*) FROM issues WHERE id = ?", "i1").Scan(&count) + if err := db.QueryRow("SELECT COUNT(*) FROM issues WHERE id = ?", "i1").Scan(&count); err != nil { + t.Fatalf("count issue: %v", err) + } if count != 0 { t.Fatalf("expected 0 rows, got %d", count) } @@ -320,7 +356,9 @@ func TestDeleteEntity_Missing(t *testing.T) { if err := deleteEntity(tx, "issues", "nonexistent"); err != nil { t.Fatalf("delete missing should not error: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } } func TestSoftDeleteEntity(t *testing.T) { @@ -328,17 +366,23 @@ func TestSoftDeleteEntity(t *testing.T) { tx := beginTx(t, db) p, _ := json.Marshal(map[string]any{"title": "soft"}) _, _ = upsertEntity(tx, "issues", "i1", p) - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } now := time.Now().UTC() tx = beginTx(t, db) if err := softDeleteEntity(tx, "issues", "i1", now); err != nil { t.Fatalf("soft delete: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } var deletedAt sql.NullTime - db.QueryRow("SELECT deleted_at FROM issues WHERE id = ?", "i1").Scan(&deletedAt) + if err := db.QueryRow("SELECT deleted_at FROM issues WHERE id = ?", "i1").Scan(&deletedAt); err != nil { + t.Fatalf("query deleted_at: %v", err) + } if !deletedAt.Valid { t.Fatal("deleted_at should be set") } @@ -350,13 +394,15 @@ func TestSoftDeleteEntity_Missing(t *testing.T) { if err := softDeleteEntity(tx, "issues", "nonexistent", time.Now()); err != nil { t.Fatalf("soft delete missing should not error: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } } func TestApplyEvent_UnknownAction(t *testing.T) { db := setupDB(t) tx := beginTx(t, db) - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() _, err := ApplyEvent(tx, Event{ ActionType: "bogus", @@ -371,7 +417,7 @@ func TestApplyEvent_UnknownAction(t *testing.T) { func TestApplyEvent_InvalidEntityType(t *testing.T) { db := setupDB(t) tx := beginTx(t, db) - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() _, err := ApplyEvent(tx, Event{ ActionType: "create", @@ -398,10 +444,14 @@ func TestApplyEvent_Create(t *testing.T) { if err != nil { t.Fatalf("apply create: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } var title string - db.QueryRow("SELECT title FROM issues WHERE id = ?", "i1").Scan(&title) + if err := db.QueryRow("SELECT title FROM issues WHERE id = ?", "i1").Scan(&title); err != nil { + t.Fatalf("query title: %v", err) + } if title != "via apply" { t.Fatalf("got title=%q", title) } @@ -414,7 +464,9 @@ func TestApplyEvent_Update(t *testing.T) { tx := beginTx(t, db) p1, _ := json.Marshal(map[string]any{"title": "orig", "status": "open"}) _, _ = ApplyEvent(tx, Event{ActionType: "create", EntityType: "issues", EntityID: "i1", Payload: p1}, testValidator) - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } // Update tx = beginTx(t, db) @@ -423,10 +475,14 @@ func TestApplyEvent_Update(t *testing.T) { if err != nil { t.Fatalf("apply update: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } var title, status string - db.QueryRow("SELECT title, status FROM issues WHERE id = ?", "i1").Scan(&title, &status) + if err := db.QueryRow("SELECT title, status FROM issues WHERE id = ?", "i1").Scan(&title, &status); err != nil { + t.Fatalf("query issue: %v", err) + } if title != "updated" || status != "closed" { t.Fatalf("got title=%q status=%q", title, status) } @@ -448,7 +504,9 @@ func TestUpsertEntity_OverwriteDetection(t *testing.T) { if res.OldData != nil { t.Fatal("first insert should have nil OldData") } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } // Second insert to same ID should be an overwrite tx = beginTx(t, db) @@ -471,7 +529,9 @@ func TestUpsertEntity_OverwriteDetection(t *testing.T) { if old["title"] != "first" { t.Fatalf("OldData title=%v, want 'first'", old["title"]) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } // Insert to different ID should not be an overwrite tx = beginTx(t, db) @@ -483,7 +543,9 @@ func TestUpsertEntity_OverwriteDetection(t *testing.T) { if res.Overwritten { t.Fatal("insert to new ID should not be an overwrite") } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } } func TestApplyEvent_OverwriteTracking(t *testing.T) { @@ -499,7 +561,9 @@ func TestApplyEvent_OverwriteTracking(t *testing.T) { if overwritten { t.Fatal("create should not report overwrite") } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } // Update same entity tx = beginTx(t, db) @@ -511,7 +575,9 @@ func TestApplyEvent_OverwriteTracking(t *testing.T) { if !overwritten { t.Fatal("update to existing entity should report overwrite") } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } } func TestUpsertEntity_LabelsArrayNormalized(t *testing.T) { @@ -524,10 +590,14 @@ func TestUpsertEntity_LabelsArrayNormalized(t *testing.T) { if err != nil { t.Fatalf("upsert with labels array: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } var labels string - db.QueryRow("SELECT labels FROM issues WHERE id = ?", "i1").Scan(&labels) + if err := db.QueryRow("SELECT labels FROM issues WHERE id = ?", "i1").Scan(&labels); err != nil { + t.Fatalf("query labels: %v", err) + } if labels != "bug,urgent" { t.Fatalf("labels: got %q, want 'bug,urgent'", labels) } @@ -542,11 +612,15 @@ func TestUpsertEntity_HandoffArraysNormalized(t *testing.T) { if err != nil { t.Fatalf("upsert handoff with arrays: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } var done, remaining, decisions, uncertain string - db.QueryRow("SELECT done, remaining, decisions, uncertain FROM handoffs WHERE id = ?", "h1"). - Scan(&done, &remaining, &decisions, &uncertain) + if err := db.QueryRow("SELECT done, remaining, decisions, uncertain FROM handoffs WHERE id = ?", "h1"). + Scan(&done, &remaining, &decisions, &uncertain); err != nil { + t.Fatalf("query handoff: %v", err) + } if done != `["task A"]` { t.Fatalf("done: got %q, want '[\"task A\"]'", done) @@ -572,10 +646,14 @@ func TestUpsertEntity_NestedObjectNormalized(t *testing.T) { if err != nil { t.Fatalf("upsert with nested object: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } var priority string - db.QueryRow("SELECT priority FROM issues WHERE id = ?", "i1").Scan(&priority) + if err := db.QueryRow("SELECT priority FROM issues WHERE id = ?", "i1").Scan(&priority); err != nil { + t.Fatalf("query priority: %v", err) + } if priority != `{"level":"high","score":5}` { t.Fatalf("priority: got %q", priority) } @@ -584,7 +662,7 @@ func TestUpsertEntity_NestedObjectNormalized(t *testing.T) { func TestGetTableColumns(t *testing.T) { db := setupDB(t) tx := beginTx(t, db) - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() cols, err := getTableColumns(tx, "issues") if err != nil { @@ -611,7 +689,9 @@ func TestUpsertEntity_UnknownFieldsIgnored(t *testing.T) { if err != nil { t.Fatalf("upsert with unknown fields: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } var title, status string err = db.QueryRow("SELECT title, status FROM issues WHERE id = ?", "i1").Scan(&title, &status) @@ -626,7 +706,7 @@ func TestUpsertEntity_UnknownFieldsIgnored(t *testing.T) { func TestUpsertEntity_AllFieldsUnknown(t *testing.T) { db := setupDB(t) tx := beginTx(t, db) - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() payload := []byte(`{"custom_xyz":"ignored","another_fake":"also ignored"}`) _, err := upsertEntity(tx, "issues", "i1", payload) @@ -688,7 +768,9 @@ func TestApplyEvent_DeferFields(t *testing.T) { if err != nil { t.Fatalf("apply create: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } // Verify all fields persisted var title, deferUntil, dueDate sql.NullString @@ -816,7 +898,9 @@ func TestApplyEvent_DeferFieldsPartialUpdate(t *testing.T) { if err != nil { t.Fatalf("create: %v", err) } - tx.Commit() + if err := tx.Commit(); err != nil { + t.Fatalf("commit: %v", err) + } // Partial update: change only defer_until via applyEventWithPrevious previousData, _ := json.Marshal(map[string]any{ @@ -941,7 +1025,7 @@ func setupDepDB(t *testing.T) *sql.DB { func TestWouldCreateCycleTx_NoCycle(t *testing.T) { db := setupDepDB(t) tx := beginTx(t, db) - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() // Add A->B _, err := tx.Exec(`INSERT INTO issue_dependencies (id, issue_id, depends_on_id, relation_type) VALUES ('d1', 'A', 'B', 'depends_on')`) @@ -958,7 +1042,7 @@ func TestWouldCreateCycleTx_NoCycle(t *testing.T) { func TestWouldCreateCycleTx_DirectCycle(t *testing.T) { db := setupDepDB(t) tx := beginTx(t, db) - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() // Add A->B _, err := tx.Exec(`INSERT INTO issue_dependencies (id, issue_id, depends_on_id, relation_type) VALUES ('d1', 'A', 'B', 'depends_on')`) @@ -975,7 +1059,7 @@ func TestWouldCreateCycleTx_DirectCycle(t *testing.T) { func TestWouldCreateCycleTx_TransitiveCycle(t *testing.T) { db := setupDepDB(t) tx := beginTx(t, db) - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() // Add A->B, B->C _, err := tx.Exec(`INSERT INTO issue_dependencies (id, issue_id, depends_on_id, relation_type) VALUES ('d1', 'A', 'B', 'depends_on')`) @@ -996,7 +1080,7 @@ func TestWouldCreateCycleTx_TransitiveCycle(t *testing.T) { func TestCheckAndResolveCyclicDependency_NoConflict(t *testing.T) { db := setupDepDB(t) tx := beginTx(t, db) - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() event := Event{ EntityType: "issue_dependencies", @@ -1012,7 +1096,7 @@ func TestCheckAndResolveCyclicDependency_NoConflict(t *testing.T) { func TestCheckAndResolveCyclicDependency_SkipsLargerKey(t *testing.T) { db := setupDepDB(t) tx := beginTx(t, db) - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() // Add B->A first (larger key) _, err := tx.Exec(`INSERT INTO issue_dependencies (id, issue_id, depends_on_id, relation_type) VALUES ('d1', 'B', 'A', 'depends_on')`) @@ -1034,7 +1118,9 @@ func TestCheckAndResolveCyclicDependency_SkipsLargerKey(t *testing.T) { // Verify B->A was removed var count int - tx.QueryRow("SELECT COUNT(*) FROM issue_dependencies WHERE issue_id='B' AND depends_on_id='A'").Scan(&count) + if err := tx.QueryRow("SELECT COUNT(*) FROM issue_dependencies WHERE issue_id='B' AND depends_on_id='A'").Scan(&count); err != nil { + t.Fatalf("count B->A: %v", err) + } if count != 0 { t.Fatalf("B->A should have been removed, got count=%d", count) } @@ -1043,7 +1129,7 @@ func TestCheckAndResolveCyclicDependency_SkipsLargerKey(t *testing.T) { func TestCheckAndResolveCyclicDependency_KeepsSmallerKey(t *testing.T) { db := setupDepDB(t) tx := beginTx(t, db) - defer tx.Rollback() + defer func() { _ = tx.Rollback() }() // Add A->B first (smaller key) _, err := tx.Exec(`INSERT INTO issue_dependencies (id, issue_id, depends_on_id, relation_type) VALUES ('d1', 'A', 'B', 'depends_on')`) @@ -1065,7 +1151,9 @@ func TestCheckAndResolveCyclicDependency_KeepsSmallerKey(t *testing.T) { // Verify A->B still exists var count int - tx.QueryRow("SELECT COUNT(*) FROM issue_dependencies WHERE issue_id='A' AND depends_on_id='B'").Scan(&count) + if err := tx.QueryRow("SELECT COUNT(*) FROM issue_dependencies WHERE issue_id='A' AND depends_on_id='B'").Scan(&count); err != nil { + t.Fatalf("count A->B: %v", err) + } if count != 1 { t.Fatalf("A->B should still exist, got count=%d", count) } diff --git a/pkg/monitor/notes_modal.go b/pkg/monitor/notes_modal.go index 5e845acb..05388c7a 100644 --- a/pkg/monitor/notes_modal.go +++ b/pkg/monitor/notes_modal.go @@ -14,25 +14,56 @@ import ( "github.com/marcus/td/pkg/monitor/mouse" ) +var ( + _ = NotesDataMsg{} + _ = NoteDetailMsg{} + _ = NoteMarkdownRenderedMsg{} + _ = NoteSavedMsg{} + _ = NoteDeletedMsg{} + _ = NotePinToggledMsg{} + _ = NoteArchivedMsg{} + _ = Model.openNotesModal + _ = (*Model).closeNotesModal + _ = Model.fetchNotes + _ = Model.fetchNotesWithArchived + _ = Model.renderNoteMarkdownAsync + _ = (*Model).createNotesListModal + _ = (*Model).createNoteDetailModal + _ = (*Model).createNoteEditModal + _ = (*Model).createNoteDeleteConfirmModal + _ = Model.handleNotesAction + _ = Model.openNoteCreator + _ = Model.openNoteEditor + _ = Model.saveNote + _ = Model.cancelNoteEdit + _ = Model.toggleNotePin + _ = Model.toggleNoteArchive + _ = Model.renderNotesModal + _ = Model.wrapSimpleModal + _ = formatNoteListItem + _ = formatNoteMeta + _ = formatNoteAge +) + // --- Notes Modal State --- // NotesState holds the state for the notes modal system. type NotesState struct { // List state - Notes []models.Note - ListCursor int + Notes []models.Note + ListCursor int ShowArchived bool // Detail state - DetailNote *models.Note - DetailRender string // Pre-rendered markdown content + DetailNote *models.Note + DetailRender string // Pre-rendered markdown content // Edit state - Editing bool - Creating bool - EditTitle *textinput.Model - EditContent *textarea.Model - EditNoteID string // ID of note being edited (empty for create) + Editing bool + Creating bool + EditTitle *textinput.Model + EditContent *textarea.Model + EditNoteID string // ID of note being edited (empty for create) // Delete confirmation DeleteConfirm bool