From 8c0cca25bd5aab761e6ca29c76fa07eed6722ce7 Mon Sep 17 00:00:00 2001 From: Review Bot Date: Sat, 18 Apr 2026 03:00:02 -0700 Subject: [PATCH] Fix lint errcheck issues Nightshift-Task: lint-fix Nightshift-Ref: https://github.com/marcus/nightshift --- cmd/block_test.go | 4 +- cmd/cascade_cli_test.go | 74 ++--- cmd/context_test.go | 48 ++-- cmd/create.go | 5 +- cmd/create_test.go | 16 +- cmd/defer.go | 12 +- cmd/delete_test.go | 32 +-- cmd/dependencies_test.go | 164 +++++------ cmd/due.go | 12 +- cmd/epic.go | 4 +- cmd/epic_test.go | 2 +- cmd/errors_test.go | 6 +- cmd/focus_test.go | 46 +-- cmd/handoff.go | 6 +- cmd/handoff_test.go | 30 +- cmd/init.go | 24 +- cmd/link.go | 6 +- cmd/log_test.go | 32 +-- cmd/review.go | 20 +- cmd/review_test.go | 48 ++-- cmd/root.go | 4 +- cmd/search_test.go | 42 +-- cmd/security_test.go | 2 +- cmd/show_test.go | 4 +- cmd/start.go | 16 +- cmd/start_test.go | 32 +-- cmd/sync.go | 20 +- cmd/sync_tail.go | 4 +- cmd/sync_tail_test.go | 6 +- cmd/system.go | 14 +- cmd/task.go | 4 +- cmd/test_helpers_test.go | 10 + cmd/tree_test.go | 78 ++--- cmd/undo_test.go | 18 +- cmd/unstart.go | 6 +- cmd/unstart_test.go | 2 +- cmd/update.go | 24 +- cmd/update_test.go | 118 ++++---- cmd/ws.go | 71 +++-- cmd/ws_test.go | 110 +++---- internal/agent/instructions_test.go | 40 +-- internal/agent/test_helpers_test.go | 10 + internal/db/activity_test.go | 6 +- internal/db/ids.go | 2 +- internal/db/issue_relations_test.go | 1 - internal/db/sync_state_test.go | 6 +- internal/query/ast.go | 12 +- internal/query/execute.go | 2 +- internal/query/execute_test.go | 6 +- internal/query/lexer_test.go | 4 +- internal/serve/portfile_unix.go | 2 +- internal/serve/response_test.go | 10 +- internal/serve/session.go | 10 +- internal/serve/sse.go | 6 +- internal/serve/test_helpers_test.go | 10 + internal/serverdb/auth_events.go | 12 +- internal/session/agent_fingerprint_test.go | 62 ++-- internal/session/session.go | 8 +- internal/sync/backfill_test.go | 16 +- internal/sync/client_test.go | 64 ++--- internal/sync/engine_test.go | 28 +- internal/sync/events_test.go | 118 ++++---- internal/sync/test_helpers_test.go | 10 + internal/syncclient/client.go | 4 +- internal/version/semver_test.go | 8 +- internal/version/version_test.go | 16 +- internal/workdir/associations_test.go | 4 +- pkg/monitor/clipboard_test.go | 4 +- pkg/monitor/data_test.go | 2 +- pkg/monitor/input.go | 1 - pkg/monitor/input_test.go | 320 ++++++++++----------- pkg/monitor/keymap/registry.go | 76 ++--- pkg/monitor/model.go | 6 +- pkg/monitor/model_test.go | 8 +- pkg/monitor/notes_lint_refs.go | 28 ++ pkg/monitor/notes_modal.go | 18 +- pkg/monitor/submit_to_review_test.go | 106 +++---- pkg/monitor/sync_prompt.go | 2 +- pkg/monitor/sync_prompt_test.go | 1 - pkg/monitor/types.go | 36 +-- test/e2e/engine.go | 12 +- test/e2e/history.go | 16 +- test/e2e/random.go | 10 +- test/e2e/report.go | 28 +- test/e2e/restart.go | 1 - test/e2e/verify.go | 6 +- test/syncharness/field_merge_test.go | 6 +- test/syncharness/harness.go | 8 +- test/syncharness/harness_test.go | 8 +- test/syncharness/server_migration_test.go | 6 +- 90 files changed, 1265 insertions(+), 1097 deletions(-) create mode 100644 cmd/test_helpers_test.go create mode 100644 internal/agent/test_helpers_test.go create mode 100644 internal/serve/test_helpers_test.go create mode 100644 internal/sync/test_helpers_test.go create mode 100644 pkg/monitor/notes_lint_refs.go diff --git a/cmd/block_test.go b/cmd/block_test.go index 2a659c28..1f3e7e7b 100644 --- a/cmd/block_test.go +++ b/cmd/block_test.go @@ -84,8 +84,8 @@ func TestBlockFromDifferentStatuses(t *testing.T) { defer database.Close() testCases := []struct { - name string - initialStatus models.Status + name string + initialStatus models.Status shouldTransition bool }{ {"from open", models.StatusOpen, true}, diff --git a/cmd/cascade_cli_test.go b/cmd/cascade_cli_test.go index 0f273734..4347ddad 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) + must(t, database.CreateIssue(epic)) child := &models.Issue{Title: "Task: Implement", Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID} - database.CreateIssue(child) + must(t, database.CreateIssue(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) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(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]) + must(t, database.CreateIssue(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]) + must(t, database.UpdateIssue(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]) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(grandparent)) parent := &models.Issue{Title: "Epic: L2", Type: models.TypeEpic, Status: models.StatusOpen, ParentID: grandparent.ID} - database.CreateIssue(parent) + must(t, database.CreateIssue(parent)) child := &models.Issue{Title: "Task: L3", Type: models.TypeTask, Status: models.StatusOpen, ParentID: parent.ID} - database.CreateIssue(child) + must(t, database.CreateIssue(child)) now := time.Now() child.Status = models.StatusClosed child.ClosedAt = &now - database.UpdateIssue(child) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(epic)) child := &models.Issue{Title: "Child for review", Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID} - database.CreateIssue(child) + must(t, database.CreateIssue(child)) child.Status = models.StatusInReview - database.UpdateIssue(child) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(task)) now := time.Now() task.Status = models.StatusClosed task.ClosedAt = &now - database.UpdateIssue(task) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(epic)) child := &models.Issue{Title: "Child for undo", Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID} - database.CreateIssue(child) + must(t, database.CreateIssue(child)) now := time.Now() child.Status = models.StatusClosed child.ClosedAt = &now - database.UpdateIssue(child) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(parentTask)) childTask := &models.Issue{Title: "Child Task", Type: models.TypeTask, Status: models.StatusOpen, ParentID: parentTask.ID} - database.CreateIssue(childTask) + must(t, database.CreateIssue(childTask)) now := time.Now() childTask.Status = models.StatusClosed childTask.ClosedAt = &now - database.UpdateIssue(childTask) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(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]) + must(t, database.CreateIssue(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]) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(epic)) child1 := &models.Issue{Title: "Child 1", Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID} - database.CreateIssue(child1) + must(t, database.CreateIssue(child1)) child2 := &models.Issue{Title: "Child 2", Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID} - database.CreateIssue(child2) + must(t, database.CreateIssue(child2)) child1.Status = models.StatusInReview - database.UpdateIssue(child1) + must(t, database.UpdateIssue(child1)) now := time.Now() child2.Status = models.StatusClosed child2.ClosedAt = &now - database.UpdateIssue(child2) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(epic)) child1 := &models.Issue{Title: "Open", Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID} - database.CreateIssue(child1) + must(t, database.CreateIssue(child1)) child2 := &models.Issue{Title: "InProgress", Type: models.TypeTask, Status: models.StatusInProgress, ParentID: epic.ID} - database.CreateIssue(child2) + must(t, database.CreateIssue(child2)) child3 := &models.Issue{Title: "Closed", Type: models.TypeTask, Status: models.StatusClosed, ParentID: epic.ID} now := time.Now() child3.ClosedAt = &now - database.CreateIssue(child3) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(epic)) child := &models.Issue{Title: "Orphan child", Type: models.TypeTask, Status: models.StatusOpen, ParentID: epic.ID} - database.CreateIssue(child) + must(t, database.CreateIssue(child)) child.Status = models.StatusClosed child.ClosedAt = &now - database.UpdateIssue(child) + must(t, database.UpdateIssue(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..e83d115f 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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) + must(t, database.CreateIssue(issue3)) // Resume each in sequence - config.SetFocus(dir, issue1.ID) + must(t, config.SetFocus(dir, issue1.ID)) focused1, _ := config.GetFocus(dir) if focused1 != issue1.ID { t.Error("Focus should be issue1") } - config.SetFocus(dir, issue2.ID) + must(t, config.SetFocus(dir, issue2.ID)) focused2, _ := config.GetFocus(dir) if focused2 != issue2.ID { t.Error("Focus should be issue2") } - config.SetFocus(dir, issue3.ID) + must(t, config.SetFocus(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) + must(t, database.CreateIssue(issue)) // Resume and retrieve context - config.SetFocus(dir, issue.ID) + must(t, config.SetFocus(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) + must(t, database.CreateIssue(issue)) - config.SetFocus(dir, issue.ID) + must(t, config.SetFocus(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) + must(t, database.CreateIssue(issue)) // Can still resume closed issue for context - config.SetFocus(dir, issue.ID) + must(t, config.SetFocus(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) + must(t, database.CreateIssue(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) + must(t, database.AddLog(log)) } // Resume and verify logs are accessible - config.SetFocus(dir, issue.ID) + must(t, config.SetFocus(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) + must(t, database.CreateIssue(parent)) child := &models.Issue{ Title: "Child Task", ParentID: parent.ID, Type: models.TypeTask, } - database.CreateIssue(child) + must(t, database.CreateIssue(child)) // Resume child - config.SetFocus(dir, child.ID) + must(t, config.SetFocus(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) + must(t, database.CreateIssue(prerequisite)) + must(t, database.CreateIssue(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) + must(t, config.SetFocus(dir, dependent.ID)) // Verify dependency preserved deps, _ := database.GetDependencies(dependent.ID) diff --git a/cmd/create.go b/cmd/create.go index 09f9b009..52065a4d 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -33,7 +33,10 @@ 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 { + output.Error("failed to set type flag: %v", err) + return err + } } args = args[1:] } diff --git a/cmd/create_test.go b/cmd/create_test.go index 029f0c0d..136a17ef 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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(prereq1)) + must(t, database.CreateIssue(prereq2)) + must(t, database.CreateIssue(prereq3)) // Create dependent issue dependent := &models.Issue{Title: "Dependent"} - database.CreateIssue(dependent) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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", "") + must(t, createCmd.Flags().Set("notes", "")) } // TestCreateTagFlagParsing tests that --tag and --tags flags are defined and work diff --git a/cmd/defer.go b/cmd/defer.go index b8e05095..ffdf904e 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: %v", 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: %v", 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..b6d7a23a 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) + must(t, database.CreateIssue(issue)) issueIDs = append(issueIDs, issue.ID) } @@ -95,7 +95,7 @@ func TestDeleteLogsAction(t *testing.T) { Title: "Test Issue", Status: models.StatusOpen, } - database.CreateIssue(issue) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(issue)) // Delete once - database.DeleteIssue(issue.ID) + must(t, database.DeleteIssue(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) + must(t, database.CreateIssue(parent)) + must(t, database.CreateIssue(child)) // Add dependency - database.AddDependency(child.ID, parent.ID, "depends_on") + must(t, database.AddDependency(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) + must(t, database.CreateIssue(issue)) if issue.DeletedAt != nil { t.Error("DeletedAt should be nil before delete") } // Delete - database.DeleteIssue(issue.ID) + must(t, database.DeleteIssue(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) + must(t, database.CreateIssue(issue)) issueID := issue.ID // Delete the issue - database.DeleteIssue(issueID) + must(t, database.DeleteIssue(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) + must(t, database.CreateIssue(issue)) issueIDs = append(issueIDs, issue.ID) } // Delete all for _, id := range issueIDs { - database.DeleteIssue(id) + must(t, database.DeleteIssue(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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) // Delete first issue successfully err1 := database.DeleteIssue(issue1.ID) diff --git a/cmd/dependencies_test.go b/cmd/dependencies_test.go index f34d2a1a..fcc5c82c 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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) // Add issue2 depends on issue1 - database.AddDependency(issue2.ID, issue1.ID, "depends_on") + must(t, database.AddDependency(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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) + must(t, database.CreateIssue(issue3)) - database.AddDependency(issue2.ID, issue1.ID, "depends_on") - database.AddDependency(issue3.ID, issue2.ID, "depends_on") + must(t, database.AddDependency(issue2.ID, issue1.ID, "depends_on")) + must(t, database.AddDependency(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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) + must(t, database.CreateIssue(issue3)) // issue2 depends on issue1 (no cycle yet) - database.AddDependency(issue2.ID, issue1.ID, "depends_on") + must(t, database.AddDependency(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(blocker)) + must(t, database.CreateIssue(blocked1)) + must(t, database.CreateIssue(blocked2)) // Both blocked1 and blocked2 depend on blocker - database.AddDependency(blocked1.ID, blocker.ID, "depends_on") - database.AddDependency(blocked2.ID, blocker.ID, "depends_on") + must(t, database.AddDependency(blocked1.ID, blocker.ID, "depends_on")) + must(t, database.AddDependency(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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) + must(t, database.CreateIssue(issue3)) + must(t, database.CreateIssue(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") + must(t, database.AddDependency(issue2.ID, issue1.ID, "depends_on")) + must(t, database.AddDependency(issue3.ID, issue2.ID, "depends_on")) + must(t, database.AddDependency(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) + must(t, database.CreateIssue(top)) + must(t, database.CreateIssue(mid1)) + must(t, database.CreateIssue(mid2)) + must(t, database.CreateIssue(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") + must(t, database.AddDependency(mid1.ID, top.ID, "depends_on")) + must(t, database.AddDependency(mid2.ID, top.ID, "depends_on")) + must(t, database.AddDependency(bottom.ID, mid1.ID, "depends_on")) + must(t, database.AddDependency(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) + must(t, database.CreateIssue(issue3)) - database.AddDependency(issue2.ID, issue1.ID, "depends_on") - database.AddDependency(issue3.ID, issue2.ID, "depends_on") + must(t, database.AddDependency(issue2.ID, issue1.ID, "depends_on")) + must(t, database.AddDependency(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) + must(t, database.CreateIssue(openIssue)) + must(t, database.CreateIssue(closedIssue)) issueMap := map[string]*models.Issue{ openIssue.ID: openIssue, @@ -363,7 +363,7 @@ func TestDepAddDependsOnFlag(t *testing.T) { } // Reset - depAddCmd.Flags().Set("depends-on", "") + must(t, depAddCmd.Flags().Set("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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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") + must(t, database.AddDependency(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") + must(t, database.AddDependency(issues[1].ID, issues[0].ID, "depends_on")) + must(t, database.AddDependency(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") + must(t, database.AddDependency(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]) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(issue)) return issue.ID, "nonexistent" }, wantError: true, @@ -597,10 +597,10 @@ 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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) // Add dependency first time - addDependency(database, issue1.ID, issue2.ID, "ses_test") + must(t, addDependency(database, issue1.ID, issue2.ID, "ses_test")) return issue1.ID, issue2.ID }, wantError: false, // addDependency returns nil for duplicates (with warning) @@ -611,8 +611,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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) return issue1.ID, issue2.ID }, wantError: false, @@ -623,8 +623,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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) return issue2.ID, issue1.ID }, wantError: false, @@ -635,8 +635,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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) return issue2.ID, issue1.ID }, wantError: false, @@ -674,13 +674,13 @@ 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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) + must(t, database.CreateIssue(issue3)) // Add dependencies - addDependency(database, issue2.ID, issue1.ID, "ses_test") - addDependency(database, issue3.ID, issue2.ID, "ses_test") + must(t, addDependency(database, issue2.ID, issue1.ID, "ses_test")) + must(t, addDependency(database, issue3.ID, issue2.ID, "ses_test")) database.Close() @@ -716,10 +716,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") + must(t, database.AddDependency(issues["B"].ID, issues["A"].ID, "depends_on")) + must(t, database.AddDependency(issues["C"].ID, issues["A"].ID, "depends_on")) + must(t, database.AddDependency(issues["D"].ID, issues["B"].ID, "depends_on")) + must(t, database.AddDependency(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 +740,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") + must(t, database.AddDependency(issues["B"].ID, issues["A"].ID, "depends_on")) + must(t, database.AddDependency(issues["C"].ID, issues["B"].ID, "depends_on")) + must(t, database.AddDependency(issues["D"].ID, issues["C"].ID, "depends_on")) + must(t, database.AddDependency(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 +765,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") + must(t, database.AddDependency(issues["B"].ID, issues["A"].ID, "depends_on")) + must(t, database.AddDependency(issues["C"].ID, issues["A"].ID, "depends_on")) + must(t, database.AddDependency(issues["D"].ID, issues["A"].ID, "depends_on")) + must(t, database.AddDependency(issues["E"].ID, issues["A"].ID, "depends_on")) + must(t, database.AddDependency(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 +805,7 @@ func TestAddDependencyComplexGraph(t *testing.T) { Title: fmt.Sprintf("Issue %s", label), Status: models.StatusOpen, } - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) issues[label] = issue } diff --git a/cmd/due.go b/cmd/due.go index f3293ea2..b642b9e9 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: %v", 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: %v", 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..fbdc3c90 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", "") + must(t, epicCreateCmd.Flags().Set("type", "")) } diff --git a/cmd/errors_test.go b/cmd/errors_test.go index e94aeee6..977f3fed 100644 --- a/cmd/errors_test.go +++ b/cmd/errors_test.go @@ -96,9 +96,9 @@ func TestErrorsCommandCount(t *testing.T) { defer database.Close() tests := []struct { - name string - numErrs int - wantCnt int + name string + numErrs int + wantCnt int }{ { name: "no errors", diff --git a/cmd/focus_test.go b/cmd/focus_test.go index 3a7eda78..5d0272cd 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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) // Focus on issue 1 - config.SetFocus(dir, issue1.ID) + must(t, config.SetFocus(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) + must(t, config.SetFocus(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) + must(t, database.CreateIssue(issue)) // Set focus - config.SetFocus(dir, issue.ID) + must(t, config.SetFocus(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(issue)) // Set focus - config.SetFocus(dir, issue.ID) + must(t, config.SetFocus(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(issue)) originalID := issue.ID // Set focus - config.SetFocus(dir, originalID) + must(t, config.SetFocus(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) + must(t, database.CreateIssue(issue)) // Set focus - config.SetFocus(dir, issue.ID) + must(t, config.SetFocus(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) + must(t, database.CreateIssue(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) + must(t, config.SetFocus(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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) + must(t, database.CreateIssue(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) + must(t, config.SetFocus(dir, issue1.ID)) + must(t, config.SetFocus(dir, issue2.ID)) + must(t, config.SetFocus(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..7ec7e2fa 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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.AddHandoff(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) + must(t, database.CreateIssue(issue)) originalUpdatedAt := issue.UpdatedAt // Update issue (as handoff command would) - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.AddHandoff(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) + must(t, database.AddHandoff(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", "") + must(t, handoffCmd.Flags().Set("note", "")) } // TestGetLatestHandoff tests retrieving the most recent handoff @@ -358,20 +358,20 @@ func TestGetLatestHandoff(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue", Status: models.StatusInProgress} - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) // Add handoffs - database.AddHandoff(&models.Handoff{ + must(t, database.AddHandoff(&models.Handoff{ IssueID: issue.ID, SessionID: "ses_old", Done: []string{"Old work"}, - }) + })) - database.AddHandoff(&models.Handoff{ + must(t, database.AddHandoff(&models.Handoff{ IssueID: issue.ID, SessionID: "ses_new", Done: []string{"New work"}, - }) + })) // Get latest latest, err := database.GetLatestHandoff(issue.ID) @@ -441,7 +441,7 @@ func TestHandoffMessageFlag(t *testing.T) { } // Reset - handoffCmd.Flags().Set("message", "") + must(t, handoffCmd.Flags().Set("message", "")) } // TestCascadeHandoffBasic tests that handoff cascades to children diff --git a/cmd/init.go b/cmd/init.go index 4b7132b5..6ed6e0d8 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -43,7 +43,9 @@ var initCmd = &cobra.Command{ // Add to .gitignore if in a git repo if git.IsRepo() { gitignorePath := filepath.Join(baseDir, ".gitignore") - addToGitignore(gitignorePath) + if err := addToGitignore(gitignorePath); err != nil { + output.Warning("failed to update .gitignore: %v", err) + } } // Create session @@ -62,30 +64,38 @@ var initCmd = &cobra.Command{ }, } -func addToGitignore(path string) { +func addToGitignore(path string) error { // Read existing content - content, _ := os.ReadFile(path) + content, err := os.ReadFile(path) + if err != nil && !os.IsNotExist(err) { + return err + } contentStr := string(content) // Check if already present if strings.Contains(contentStr, ".todos/") { - return + return nil } // Append to file f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - return + return err } defer f.Close() // 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 err + } } - f.WriteString(".todos/\n") + if _, err := f.WriteString(".todos/\n"); err != nil { + return err + } fmt.Println("Added .todos/ to .gitignore") + return nil } func suggestAgentFileAddition(baseDir string) { 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/log_test.go b/cmd/log_test.go index 80ade049..efa009e8 100644 --- a/cmd/log_test.go +++ b/cmd/log_test.go @@ -55,7 +55,7 @@ func TestLogMultipleMessages(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) messages := []string{ "Initial exploration", @@ -71,7 +71,7 @@ func TestLogMultipleMessages(t *testing.T) { Message: msg, Type: models.LogTypeProgress, } - database.AddLog(log) + must(t, database.AddLog(log)) } logs, _ := database.GetLogs(issue.ID, 10) @@ -102,7 +102,7 @@ func TestLogWithDifferentTypes(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) testCases := []struct { name string @@ -157,7 +157,7 @@ func TestLogRetrievalLimit(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) // Add 10 logs for i := 0; i < 10; i++ { @@ -167,7 +167,7 @@ func TestLogRetrievalLimit(t *testing.T) { Message: "Message " + string(rune(i)), Type: models.LogTypeProgress, } - database.AddLog(log) + must(t, database.AddLog(log)) } // Test different limits @@ -201,8 +201,8 @@ func TestLogForMultipleIssues(t *testing.T) { issue1 := &models.Issue{Title: "Issue 1"} issue2 := &models.Issue{Title: "Issue 2"} - database.CreateIssue(issue1) - database.CreateIssue(issue2) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) // Add logs to issue 1 for i := 0; i < 3; i++ { @@ -212,7 +212,7 @@ func TestLogForMultipleIssues(t *testing.T) { Message: "Issue 1 log", Type: models.LogTypeProgress, } - database.AddLog(log) + must(t, database.AddLog(log)) } // Add logs to issue 2 @@ -223,7 +223,7 @@ func TestLogForMultipleIssues(t *testing.T) { Message: "Issue 2 log", Type: models.LogTypeProgress, } - database.AddLog(log) + must(t, database.AddLog(log)) } logs1, _ := database.GetLogs(issue1.ID, 10) @@ -259,7 +259,7 @@ func TestLogWithMultipleSessions(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) sessions := []string{"ses_aaa", "ses_bbb", "ses_ccc"} @@ -270,7 +270,7 @@ func TestLogWithMultipleSessions(t *testing.T) { Message: "Log from " + session, Type: models.LogTypeProgress, } - database.AddLog(log) + must(t, database.AddLog(log)) } logs, _ := database.GetLogs(issue.ID, 10) @@ -300,7 +300,7 @@ func TestLogWithWorkSession(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) workSessionID := "ws_12345" log := &models.Log{ @@ -334,7 +334,7 @@ func TestLogEmptyMessage(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) log := &models.Log{ IssueID: issue.ID, @@ -360,7 +360,7 @@ func TestLogLongMessage(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) // Create a long message longMessage := "" @@ -421,7 +421,7 @@ func TestLogRetrieval(t *testing.T) { defer database.Close() issue := &models.Issue{Title: "Test Issue"} - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) // Add logs in specific order messages := []string{"First", "Second", "Third"} @@ -432,7 +432,7 @@ func TestLogRetrieval(t *testing.T) { Message: msg, Type: models.LogTypeProgress, } - database.AddLog(log) + must(t, database.AddLog(log)) } logs, _ := database.GetLogs(issue.ID, 10) diff --git a/cmd/review.go b/cmd/review.go index 89b7bb4f..007c9db2 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: %v", 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 log security event: %v", 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 log security event: %v", err) + } } else if reason != "" { logMsg = "Closed: " + reason } diff --git a/cmd/review_test.go b/cmd/review_test.go index 2b861989..9447e8a6 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) + must(t, database.UpdateIssue(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) + must(t, database.UpdateIssue(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") + must(t, reviewCmd.Flags().Set("minor", "false")) } func TestReviewReasonShorthand(t *testing.T) { @@ -1071,7 +1071,7 @@ func TestReviewReasonShorthand(t *testing.T) { } // Reset - reviewCmd.Flags().Set("reason", "") + must(t, reviewCmd.Flags().Set("reason", "")) } func TestApproveReasonShorthand(t *testing.T) { @@ -1135,7 +1135,7 @@ func TestCloseSelfCloseExceptionRequiresValue(t *testing.T) { } // Reset flag to default before test - flag.Value.Set("") + must(t, flag.Value.Set("")) // 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("") + must(t, flag.Value.Set("")) } 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) + must(t, database.UpdateIssue(issueWithImpl)) retrieved, _ := database.GetIssue(issueWithImpl.ID) if retrieved.ImplementerSession != sessionID { @@ -1235,18 +1235,18 @@ func TestCloseSelfCloseExceptionLogMessage(t *testing.T) { if err := database.CreateIssue(issue); err != nil { t.Fatalf("CreateIssue failed: %v", err) } - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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{ + must(t, database.AddLog(&models.Log{ IssueID: issue.ID, SessionID: sessionID, Message: logMsg, Type: models.LogTypeSecurity, - }) + })) // Verify log contains exception logs, _ := database.GetLogs(issue.ID, 0) @@ -1693,19 +1693,19 @@ func TestApproveAutoUnblocksDependents(t *testing.T) { Status: models.StatusInReview, ImplementerSession: "ses_impl", } - database.CreateIssue(blocker) + must(t, database.CreateIssue(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") + must(t, database.CreateIssue(dependent)) + must(t, database.AddDependency(dependent.ID, blocker.ID, "depends_on")) // Simulate approve: close the blocker then cascade unblock blocker.Status = models.StatusClosed - database.UpdateIssue(blocker) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(blocker)) dependent := &models.Issue{ Title: "Dependent", Status: models.StatusBlocked, } - database.CreateIssue(dependent) - database.AddDependency(dependent.ID, blocker.ID, "depends_on") + must(t, database.CreateIssue(dependent)) + must(t, database.AddDependency(dependent.ID, blocker.ID, "depends_on")) // Simulate close: set closed then cascade unblock blocker.Status = models.StatusClosed - database.UpdateIssue(blocker) + must(t, database.UpdateIssue(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") + must(t, database.CreateIssue(a1)) + must(t, database.CreateIssue(a2)) + must(t, database.CreateIssue(dependent)) + must(t, database.AddDependency(dependent.ID, a1.ID, "depends_on")) + must(t, database.AddDependency(dependent.ID, a2.ID, "depends_on")) // Approve only A1 a1.Status = models.StatusClosed - database.UpdateIssue(a1) + must(t, database.UpdateIssue(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..3a6dee7e 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 { + slog.Debug("log agent error", "err", 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..06d24133 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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(issue)) opts := db.ListIssuesOptions{ Search: "database", @@ -113,8 +113,8 @@ func TestSearchByLabel(t *testing.T) { Status: models.StatusOpen, } - database.CreateIssue(issue1) - database.CreateIssue(issue2) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) opts := db.ListIssuesOptions{ Search: "backend", @@ -154,7 +154,7 @@ func TestSearchNoResults(t *testing.T) { Title: "Test Issue", Status: models.StatusOpen, } - database.CreateIssue(issue) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) opts := db.ListIssuesOptions{ Search: "issue", @@ -227,8 +227,8 @@ func TestSearchWithTypeFilter(t *testing.T) { Status: models.StatusOpen, } - database.CreateIssue(issue1) - database.CreateIssue(issue2) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) opts := db.ListIssuesOptions{ Search: "feature", @@ -267,8 +267,8 @@ func TestSearchWithPriorityFilter(t *testing.T) { Status: models.StatusOpen, } - database.CreateIssue(issue1) - database.CreateIssue(issue2) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) opts := db.ListIssuesOptions{ Search: "bug", @@ -300,7 +300,7 @@ func TestSearchWithLimit(t *testing.T) { Title: "Test issue", Status: models.StatusOpen, } - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) } opts := db.ListIssuesOptions{ @@ -339,8 +339,8 @@ func TestSearchRelevanceScoring(t *testing.T) { Status: models.StatusOpen, } - database.CreateIssue(exactMatch) - database.CreateIssue(descMatch) + must(t, database.CreateIssue(exactMatch)) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) opts := db.ListIssuesOptions{ Search: "database", @@ -442,7 +442,7 @@ func TestSearchEmptyQuery(t *testing.T) { Title: "Test Issue", Status: models.StatusOpen, } - database.CreateIssue(issue) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(issue)) opts := db.ListIssuesOptions{ Search: "database", diff --git a/cmd/security_test.go b/cmd/security_test.go index 15c12277..fb5e4c39 100644 --- a/cmd/security_test.go +++ b/cmd/security_test.go @@ -75,7 +75,7 @@ func TestCloseCommandSecurityLogging(t *testing.T) { defer os.RemoveAll(baseDir) // Ensure .todos directory exists for session - os.MkdirAll(filepath.Join(baseDir, ".todos"), 0755) + must(t, os.MkdirAll(filepath.Join(baseDir, ".todos"), 0755)) // Init DB database, err := db.Initialize(baseDir) diff --git a/cmd/show_test.go b/cmd/show_test.go index 20ac04a3..040d7659 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", "") + must(t, showCmd.Flags().Set("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") + must(t, showCmd.Flags().Set("json", "false")) } // TestShowRenderMarkdownFlagExists tests that --render-markdown flag is defined diff --git a/cmd/start.go b/cmd/start.go index cd0795e2..7460fea2 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: %v", 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: %v", 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: %v", err) + } } // Show git state once at the end diff --git a/cmd/start_test.go b/cmd/start_test.go index f4918555..71fb6822 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) + must(t, database.CreateIssue(issue)) } // Start all issues for _, issue := range issues { issue.Status = models.StatusInProgress issue.ImplementerSession = "ses_test" - database.UpdateIssue(issue) + must(t, database.UpdateIssue(issue)) } // Verify all started @@ -88,7 +88,7 @@ func TestStartBlockedIssueWithoutForce(t *testing.T) { Title: "Blocked Issue", Status: models.StatusBlocked, } - database.CreateIssue(issue) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.UpdateIssue(issue)) } retrieved, _ := database.GetIssue(issue.ID) @@ -142,12 +142,12 @@ func TestStartSetsImplementerSession(t *testing.T) { Title: "Test Issue", Status: models.StatusOpen, } - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) sessionID := "ses_abc123" issue.Status = models.StatusInProgress issue.ImplementerSession = sessionID - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(issue)) if tc.canStart || tc.initialStatus != models.StatusBlocked { issue.Status = models.StatusInProgress - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.AddLog(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) + must(t, database.CreateIssue(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) + must(t, database.UpdateIssue(validIssue)) retrieved, _ := database.GetIssue(validIssue.ID) if retrieved.Status != models.StatusInProgress { diff --git a/cmd/sync.go b/cmd/sync.go index 83225987..f737971c 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 renameErr := os.Rename(backupPath, dbPath); renameErr != nil { + return nil, fmt.Errorf("write snapshot: %w (restore backup: %v)", err, renameErr) + } 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 renameErr := os.Rename(backupPath, dbPath); renameErr != nil { + return nil, fmt.Errorf("reopen after bootstrap: %w (restore backup: %v)", err, renameErr) + } 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 renameErr := os.Rename(backupPath, dbPath); renameErr != nil { + return nil, fmt.Errorf("update sync_state: %w (restore backup: %v)", err, renameErr) + } 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.go b/cmd/sync_tail.go index 0ae83429..966cd37d 100644 --- a/cmd/sync_tail.go +++ b/cmd/sync_tail.go @@ -16,8 +16,8 @@ import ( // Styles for sync tail output var ( - pushArrow = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Render("→") // green - pullArrow = lipgloss.NewStyle().Foreground(lipgloss.Color("45")).Render("←") // cyan + pushArrow = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Render("→") // green + pullArrow = lipgloss.NewStyle().Foreground(lipgloss.Color("45")).Render("←") // cyan dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) ) diff --git a/cmd/sync_tail_test.go b/cmd/sync_tail_test.go index 8ab8cb79..b772434f 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("copy output: %v", err) + } output := buf.String() for _, s := range tt.contains { diff --git a/cmd/system.go b/cmd/system.go index 786010d3..798b2d45 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,7 +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) + if _, err := fmt.Sscanf(matches[1], "%d", &pts); err != nil { + return imported, fmt.Errorf("invalid points value %q: %w", matches[1], err) + } 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/test_helpers_test.go b/cmd/test_helpers_test.go new file mode 100644 index 00000000..aff9e115 --- /dev/null +++ b/cmd/test_helpers_test.go @@ -0,0 +1,10 @@ +package cmd + +import "testing" + +func must(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatal(err) + } +} diff --git a/cmd/tree_test.go b/cmd/tree_test.go index a21eddf2..a453d815 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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(child1)) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(root)) + must(t, database.CreateIssue(level1)) + must(t, database.CreateIssue(level2)) + must(t, database.CreateIssue(level3)) level1.ParentID = root.ID level2.ParentID = level1.ID level3.ParentID = level2.ID - database.UpdateIssue(level1) - database.UpdateIssue(level2) - database.UpdateIssue(level3) + must(t, database.UpdateIssue(level1)) + must(t, database.UpdateIssue(level2)) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(epic)) + must(t, database.CreateIssue(feature)) + must(t, database.CreateIssue(bug)) + must(t, database.CreateIssue(task)) feature.ParentID = epic.ID bug.ParentID = epic.ID task.ParentID = epic.ID - database.UpdateIssue(feature) - database.UpdateIssue(bug) - database.UpdateIssue(task) + must(t, database.UpdateIssue(feature)) + must(t, database.UpdateIssue(bug)) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(parent1)) + must(t, database.CreateIssue(parent2)) child := &models.Issue{ Title: "Child", ParentID: parent1.ID, Type: models.TypeTask, } - database.CreateIssue(child) + must(t, database.CreateIssue(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) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(parent)) statuses := []models.Status{ models.StatusOpen, @@ -293,7 +293,7 @@ func TestTreeWithDifferentStatuses(t *testing.T) { Type: models.TypeTask, Status: status, } - database.CreateIssue(child) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(parent)) priorities := []models.Priority{ models.PriorityP0, @@ -354,7 +354,7 @@ func TestTreeWithPriorities(t *testing.T) { Type: models.TypeTask, Priority: priority, } - database.CreateIssue(child) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(parent)) child := &models.Issue{ Title: "Child", ParentID: parent.ID, Type: models.TypeTask, } - database.CreateIssue(child) + must(t, database.CreateIssue(child)) // Delete parent - database.DeleteIssue(parent.ID) + must(t, database.DeleteIssue(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) + must(t, database.CreateIssue(parent)) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(child)) } // Verify root exists diff --git a/cmd/undo_test.go b/cmd/undo_test.go index 19edcb15..a9ca0eee 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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(issue)) action := &models.ActionLog{ SessionID: "ses_test", diff --git a/cmd/unstart.go b/cmd/unstart.go index c8e1f196..0dac3457 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: %v", 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..ff0e3e2f 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", "") + must(t, unstartCmd.Flags().Set("reason", "")) } diff --git a/cmd/update.go b/cmd/update.go index c6f7b8fe..88e7f113 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -202,22 +202,34 @@ 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.Error("failed to remove dependency %s: %v", dep, err) + return 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.Error("failed to add dependency %s: %v", dep, err) + return 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.Error("failed to remove blocked issue %s: %v", b, err) + return 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.Error("failed to add blocked issue %s: %v", b, err) + return err + } } } @@ -271,7 +283,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..0d093a3e 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) + must(t, database.CreateIssue(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) + must(t, database.CreateIssue(issue)) issue.Description = "New description" - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(issue)) issue.Type = models.TypeBug - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(issue)) issue.Priority = models.PriorityP0 - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(issue)) issue.Points = 8 - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(issue)) issue.Labels = []string{"new1", "new2"} - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(issue)) issue.Labels = nil - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(issue)) // Open -> In Progress issue.Status = models.StatusInProgress - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(issue)) + must(t, database.CreateIssue(dep1)) + must(t, database.CreateIssue(dep2)) // Add original dependency - database.AddDependency(issue.ID, dep1.ID, "depends_on") + must(t, database.AddDependency(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") + must(t, database.RemoveDependency(issue.ID, dep1.ID)) + must(t, database.AddDependency(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) + must(t, database.CreateIssue(issue)) + must(t, database.CreateIssue(dep1)) + must(t, database.CreateIssue(dep2)) - database.AddDependency(issue.ID, dep1.ID, "depends_on") - database.AddDependency(issue.ID, dep2.ID, "depends_on") + must(t, database.AddDependency(issue.ID, dep1.ID, "depends_on")) + must(t, database.AddDependency(issue.ID, dep2.ID, "depends_on")) // Clear all dependencies deps, _ := database.GetDependencies(issue.ID) for _, dep := range deps { - database.RemoveDependency(issue.ID, dep) + must(t, database.RemoveDependency(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) + must(t, database.CreateIssue(blocker)) + must(t, database.CreateIssue(blocked1)) + must(t, database.CreateIssue(blocked2)) // blocked1 depends on blocker - database.AddDependency(blocked1.ID, blocker.ID, "depends_on") + must(t, database.AddDependency(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") + must(t, database.RemoveDependency(blocked1.ID, blocker.ID)) + must(t, database.AddDependency(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) + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) + must(t, database.CreateIssue(issue3)) // Batch update priorities for _, issue := range []*models.Issue{issue1, issue2, issue3} { issue.Priority = models.PriorityP1 - database.UpdateIssue(issue) + must(t, database.UpdateIssue(issue)) } // Verify all updated @@ -336,11 +336,11 @@ func TestUpdatePartialFields(t *testing.T) { Priority: models.PriorityP2, Points: 5, } - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) // Only update title issue.Title = "Updated Title" - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(parent1)) + must(t, database.CreateIssue(parent2)) + must(t, database.CreateIssue(child)) // Set initial parent child.ParentID = parent1.ID - database.UpdateIssue(child) + must(t, database.UpdateIssue(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) + must(t, database.UpdateIssue(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) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(issue)) originalUpdatedAt := issue.UpdatedAt // Update issue issue.Title = "Updated" - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(issue)) issue.Acceptance = "New acceptance criteria" - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(issue)) // Simulate append mode: concat with double newline newDesc := "Appended text" issue.Description = issue.Description + "\n\n" + newDesc - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(issue)) // With empty existing description, just set the value (no leading separator) issue.Description = "New description" - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(issue)) // Simulate append mode newAC := "Additional criteria" issue.Acceptance = issue.Acceptance + "\n\n" + newAC - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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) + must(t, database.CreateIssue(issue)) // With empty existing acceptance, just set the value issue.Acceptance = "New criteria" - database.UpdateIssue(issue) + must(t, database.UpdateIssue(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", "") + must(t, updateCmd.Flags().Set("status", "")) } func TestUpdateRichTextAppendFromFileAndStdin(t *testing.T) { diff --git a/cmd/ws.go b/cmd/ws.go index dbdbecf2..363dafc8 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 auto-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 history: %v", 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: %v", 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 record git snapshot: %v", err) + } } fmt.Printf("STARTED %s (session: %s)\n", issueID, sess.ID) @@ -266,23 +278,29 @@ 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.Error("failed to add log: %v", err) + return 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.Error("failed to add log: %v", err) + return err + } // Get tagged issues for display issueIDs, _ := database.GetWorkSessionIssues(wsID) @@ -489,18 +507,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 record git snapshot: %v", err) + } } } @@ -517,8 +540,14 @@ 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.Error("failed to end work session: %v", err) + return err + } + if err := config.ClearActiveWorkSession(baseDir); err != nil { + output.Error("failed to clear active work session: %v", err) + return err + } } fmt.Printf("HANDOFF RECORDED %s\n", wsID) @@ -588,8 +617,14 @@ 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.Error("failed to end work session: %v", err) + return err + } + if err := config.ClearActiveWorkSession(baseDir); err != nil { + output.Error("failed to clear active work session: %v", err) + return 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..41b5e3a1 100644 --- a/cmd/ws_test.go +++ b/cmd/ws_test.go @@ -56,8 +56,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) + must(t, database.CreateWorkSession(ws)) + must(t, config.SetActiveWorkSession(dir, ws.ID)) // Check active session is set activeWS, err := config.GetActiveWorkSession(dir) @@ -83,11 +83,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) + must(t, database.CreateWorkSession(ws)) + must(t, config.SetActiveWorkSession(dir, ws.ID)) // End session - config.ClearActiveWorkSession(dir) + must(t, config.ClearActiveWorkSession(dir)) // Verify session is no longer active activeWS, _ := config.GetActiveWorkSession(dir) @@ -123,12 +123,12 @@ 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) + must(t, database.CreateWorkSession(ws)) + must(t, config.SetActiveWorkSession(dir, ws.ID)) // Create issue issue := &models.Issue{Title: "Test Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) // Tag issue to work session if err := database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session"); err != nil { @@ -159,7 +159,7 @@ func TestWsTagMultipleIssues(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "multi-tag-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + must(t, database.CreateWorkSession(ws)) // Create issues issues := []*models.Issue{ @@ -169,8 +169,8 @@ func TestWsTagMultipleIssues(t *testing.T) { } for _, issue := range issues { - database.CreateIssue(issue) - database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session") + must(t, database.CreateIssue(issue)) + must(t, database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session")) } // Verify all issues are tagged @@ -207,7 +207,7 @@ func TestWsTagInvalidIssueErrors(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "invalid-tag-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + must(t, database.CreateWorkSession(ws)) // Try to get non-existent issue _, err = database.GetIssue("td-nonexistent") @@ -227,12 +227,12 @@ func TestWsUntagRemovesIssueFromSession(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "untag-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + must(t, database.CreateWorkSession(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") + must(t, database.CreateIssue(issue)) + must(t, database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session")) // Verify issue is tagged issueIDs, _ := database.GetWorkSessionIssues(ws.ID) @@ -263,18 +263,18 @@ func TestWsUntagPartialRemoval(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "partial-untag-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + must(t, database.CreateWorkSession(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") + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) + must(t, database.TagIssueToWorkSession(ws.ID, issue1.ID, "test-session")) + must(t, database.TagIssueToWorkSession(ws.ID, issue2.ID, "test-session")) // Untag only issue1 - database.UntagIssueFromWorkSession(ws.ID, issue1.ID, "test-session") + must(t, database.UntagIssueFromWorkSession(ws.ID, issue1.ID, "test-session")) // Verify only issue2 remains issueIDs, _ := database.GetWorkSessionIssues(ws.ID) @@ -297,12 +297,12 @@ func TestWsLogAddsLogEntry(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "log-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + must(t, database.CreateWorkSession(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") + must(t, database.CreateIssue(issue)) + must(t, database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session")) // Add log to work session (log attached to work session, not specific issue) log := &models.Log{ @@ -340,7 +340,7 @@ func TestWsLogWithDifferentTypes(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "typed-log-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + must(t, database.CreateWorkSession(ws)) testCases := []struct { logType models.LogType @@ -398,15 +398,15 @@ func TestWsHandoffCreatesHandoffs(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "handoff-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + must(t, database.CreateWorkSession(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") + must(t, database.CreateIssue(issue1)) + must(t, database.CreateIssue(issue2)) + must(t, database.TagIssueToWorkSession(ws.ID, issue1.ID, "test-session")) + must(t, database.TagIssueToWorkSession(ws.ID, issue2.ID, "test-session")) // Create handoffs for each issue handoff1 := &models.Handoff{ @@ -467,8 +467,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) + must(t, database.CreateWorkSession(ws)) + must(t, config.SetActiveWorkSession(dir, ws.ID)) // Verify session is active activeWS, _ := config.GetActiveWorkSession(dir) @@ -477,7 +477,7 @@ func TestWsHandoffEndsSession(t *testing.T) { } // Clear active session (simulates handoff ending session) - config.ClearActiveWorkSession(dir) + must(t, config.ClearActiveWorkSession(dir)) // Verify session is ended activeWS, _ = config.GetActiveWorkSession(dir) @@ -497,13 +497,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) + must(t, database.CreateWorkSession(ws)) + must(t, config.SetActiveWorkSession(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") + must(t, database.CreateIssue(issue)) + must(t, database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session")) // Get current session activeWS, _ := config.GetActiveWorkSession(dir) @@ -551,7 +551,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) + must(t, database.CreateWorkSession(ws)) } // List work sessions @@ -597,7 +597,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) + must(t, database.CreateWorkSession(ws)) } // List with limit 5 @@ -622,11 +622,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) + must(t, database.CreateWorkSession(ws)) + must(t, config.SetActiveWorkSession(dir, ws.ID)) // End without handoff - config.ClearActiveWorkSession(dir) + must(t, config.ClearActiveWorkSession(dir)) // Verify session ended activeWS, _ := config.GetActiveWorkSession(dir) @@ -646,19 +646,19 @@ func TestWsTagAutoStartsOpenIssues(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "auto-start-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + must(t, database.CreateWorkSession(ws)) // Create open issue issue := &models.Issue{Title: "Open Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) // Tag issue (simulating auto-start behavior) - database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session") + must(t, database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session")) // Simulate starting the issue issue.Status = models.StatusInProgress issue.ImplementerSession = "ses_test" - database.UpdateIssue(issue) + must(t, database.UpdateIssue(issue)) // Verify issue is started retrieved, _ := database.GetIssue(issue.ID) @@ -678,14 +678,14 @@ func TestWsTagNoStartFlag(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "no-start-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + must(t, database.CreateWorkSession(ws)) // Create open issue issue := &models.Issue{Title: "Open Issue", Status: models.StatusOpen} - database.CreateIssue(issue) + must(t, database.CreateIssue(issue)) // Tag issue without starting (simulating --no-start) - database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session") + must(t, database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session")) // Issue should remain open (with --no-start) retrieved, _ := database.GetIssue(issue.ID) @@ -705,12 +705,12 @@ func TestWsShowDisplaysPastSession(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "past-session", SessionID: "ses_test", StartSHA: "abc123"} - database.CreateWorkSession(ws) + must(t, database.CreateWorkSession(ws)) // Tag issue issue := &models.Issue{Title: "Test Issue", Status: models.StatusInProgress} - database.CreateIssue(issue) - database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session") + must(t, database.CreateIssue(issue)) + must(t, database.TagIssueToWorkSession(ws.ID, issue.ID, "test-session")) // Get session details retrieved, err := database.GetWorkSession(ws.ID) @@ -759,7 +759,7 @@ func TestWsHandoffAutoPopulatesFromLogs(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "auto-populate-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + must(t, database.CreateWorkSession(ws)) // Add various log types logs := []models.Log{ @@ -770,7 +770,7 @@ func TestWsHandoffAutoPopulatesFromLogs(t *testing.T) { } for _, log := range logs { - database.AddLog(&log) + must(t, database.AddLog(&log)) } // Get logs for session @@ -807,7 +807,7 @@ func TestWsUpdateSession(t *testing.T) { // Create work session ws := &models.WorkSession{Name: "update-session", SessionID: "ses_test"} - database.CreateWorkSession(ws) + must(t, database.CreateWorkSession(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..79251174 100644 --- a/internal/agent/instructions_test.go +++ b/internal/agent/instructions_test.go @@ -31,8 +31,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) + must(t, os.WriteFile(filepath.Join(dir, "AGENTS.md"), []byte("# Agents"), 0644)) + must(t, os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("# Claude"), 0644)) got := DetectAgentFile(dir) if filepath.Base(got) != "AGENTS.md" { @@ -42,7 +42,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) + must(t, os.WriteFile(filepath.Join(dir, "GEMINI.md"), []byte("# Gemini"), 0644)) got := DetectAgentFile(dir) if filepath.Base(got) != "GEMINI.md" { @@ -52,7 +52,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) + must(t, os.WriteFile(filepath.Join(dir, "CLAUDE.local.md"), []byte("# Local"), 0644)) got := DetectAgentFile(dir) if filepath.Base(got) != "CLAUDE.local.md" { @@ -62,7 +62,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) + must(t, os.WriteFile(filepath.Join(dir, "CODEX.md"), []byte("# Codex"), 0644)) got := DetectAgentFile(dir) if filepath.Base(got) != "CODEX.md" { @@ -83,8 +83,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) + must(t, os.WriteFile(filepath.Join(dir, "AGENTS.md"), []byte("# Agents"), 0644)) + must(t, os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("# Claude"), 0644)) got := PreferredAgentFile(dir) if filepath.Base(got) != "AGENTS.md" { @@ -94,7 +94,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) + must(t, os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("# Claude"), 0644)) got := PreferredAgentFile(dir) if filepath.Base(got) != "CLAUDE.md" { @@ -104,7 +104,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) + must(t, os.WriteFile(filepath.Join(dir, "GEMINI.md"), []byte("# Gemini"), 0644)) got := PreferredAgentFile(dir) if filepath.Base(got) != "GEMINI.md" { @@ -114,7 +114,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) + must(t, os.WriteFile(filepath.Join(dir, "CODEX.md"), []byte("# Codex"), 0644)) got := PreferredAgentFile(dir) if filepath.Base(got) != "CODEX.md" { @@ -136,7 +136,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) + must(t, os.WriteFile(path, []byte("Run td usage --new-session"), 0644)) if !HasTDInstructions(path) { t.Error("HasTDInstructions = false, want true") @@ -146,7 +146,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) + must(t, os.WriteFile(path, []byte("# Claude instructions"), 0644)) if HasTDInstructions(path) { t.Error("HasTDInstructions = true, want false") @@ -163,7 +163,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) + must(t, os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("Run td usage --new-session"), 0644)) if !AnyFileHasTDInstructions(dir) { t.Error("AnyFileHasTDInstructions = false, want true") @@ -172,7 +172,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) + must(t, os.WriteFile(filepath.Join(dir, "GEMINI.md"), []byte("Use td usage -q"), 0644)) if !AnyFileHasTDInstructions(dir) { t.Error("AnyFileHasTDInstructions = false, want true") @@ -181,7 +181,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) + must(t, os.WriteFile(filepath.Join(dir, "CLAUDE.local.md"), []byte("td usage"), 0644)) if !AnyFileHasTDInstructions(dir) { t.Error("AnyFileHasTDInstructions = false, want true") @@ -190,7 +190,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) + must(t, os.WriteFile(filepath.Join(dir, "CODEX.md"), []byte("td usage --new-session"), 0644)) if !AnyFileHasTDInstructions(dir) { t.Error("AnyFileHasTDInstructions = false, want true") @@ -199,8 +199,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) + must(t, os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("# Claude"), 0644)) + must(t, os.WriteFile(filepath.Join(dir, "GEMINI.md"), []byte("# Gemini"), 0644)) if AnyFileHasTDInstructions(dir) { t.Error("AnyFileHasTDInstructions = true, want false") @@ -218,9 +218,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) + must(t, os.WriteFile(filepath.Join(dir, "CLAUDE.md"), []byte("# Claude"), 0644)) // GEMINI.local.md has instructions - os.WriteFile(filepath.Join(dir, "GEMINI.local.md"), []byte("td usage"), 0644) + must(t, os.WriteFile(filepath.Join(dir, "GEMINI.local.md"), []byte("td usage"), 0644)) if !AnyFileHasTDInstructions(dir) { t.Error("AnyFileHasTDInstructions = false, want true (found in GEMINI.local.md)") diff --git a/internal/agent/test_helpers_test.go b/internal/agent/test_helpers_test.go new file mode 100644 index 00000000..891f3f2e --- /dev/null +++ b/internal/agent/test_helpers_test.go @@ -0,0 +1,10 @@ +package agent + +import "testing" + +func must(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatal(err) + } +} diff --git a/internal/db/activity_test.go b/internal/db/activity_test.go index 5338971d..b815e61c 100644 --- a/internal/db/activity_test.go +++ b/internal/db/activity_test.go @@ -818,9 +818,9 @@ func TestAddHandoff_EmptyArrays(t *testing.T) { IssueID: issue.ID, SessionID: "ses_test", Done: []string{"Task 1"}, - Remaining: []string{}, // Empty - Decisions: nil, // Nil - Uncertain: []string{}, // Empty + Remaining: []string{}, // Empty + Decisions: nil, // Nil + Uncertain: []string{}, // Empty } err = db.AddHandoff(handoff) diff --git a/internal/db/ids.go b/internal/db/ids.go index 0e9df34d..84fd885e 100644 --- a/internal/db/ids.go +++ b/internal/db/ids.go @@ -16,7 +16,7 @@ const ( commentIDPrefix = "cm-" snapshotIDPrefix = "gs-" noteIDPrefix = "nt-" - actionIDPrefix = "al-" + actionIDPrefix = "al-" // Deterministic ID prefixes for composite-key tables boardIssuePosIDPrefix = "bip_" diff --git a/internal/db/issue_relations_test.go b/internal/db/issue_relations_test.go index 2d5912d1..285f61f3 100644 --- a/internal/db/issue_relations_test.go +++ b/internal/db/issue_relations_test.go @@ -1731,7 +1731,6 @@ func TestCascadeUnblockDependents_UndoData(t *testing.T) { } } - func TestGetIssueDependencyRelations(t *testing.T) { dir := t.TempDir() database, err := Initialize(dir) diff --git a/internal/db/sync_state_test.go b/internal/db/sync_state_test.go index afacd454..f4cc5560 100644 --- a/internal/db/sync_state_test.go +++ b/internal/db/sync_state_test.go @@ -16,9 +16,9 @@ func TestParseTimestamp(t *testing.T) { {"RFC3339", "2026-03-21T09:00:00Z"}, {"RFC3339Nano", now.Format(time.RFC3339Nano)}, {"Go time.String() UTC", now.String()}, - {"Go time.String() local", local.Round(0).String()}, // non-UTC, no monotonic - {"Go time.String() with monotonic", local.String()}, // includes m=+... suffix - {"non-UTC with offset", "2026-03-21 19:00:00.123456 +1000 AEST"}, // non-UTC timezone + {"Go time.String() local", local.Round(0).String()}, // non-UTC, no monotonic + {"Go time.String() with monotonic", local.String()}, // includes m=+... suffix + {"non-UTC with offset", "2026-03-21 19:00:00.123456 +1000 AEST"}, // non-UTC timezone {"RFC3339 with offset", "2026-03-21T09:00:00+00:00"}, } diff --git a/internal/query/ast.go b/internal/query/ast.go index 5aae68b6..6b49fe56 100644 --- a/internal/query/ast.go +++ b/internal/query/ast.go @@ -209,10 +209,10 @@ var CrossEntityFields = map[string]map[string]string{ // Enum values for validation var EnumValues = map[string][]string{ - "status": {"open", "in_progress", "blocked", "in_review", "closed"}, - "type": {"bug", "feature", "task", "epic", "chore"}, - "priority": {"P0", "P1", "P2", "P3", "P4"}, - "log.type": {"progress", "blocker", "decision", "hypothesis", "tried", "result", "orchestration"}, + "status": {"open", "in_progress", "blocked", "in_review", "closed"}, + "type": {"bug", "feature", "task", "epic", "chore"}, + "priority": {"P0", "P1", "P2", "P3", "P4"}, + "log.type": {"progress", "blocker", "decision", "hypothesis", "tried", "result", "orchestration"}, "file.role": {"implementation", "test", "reference", "config"}, } @@ -278,8 +278,8 @@ var NoteSortFieldToColumn = map[string]string{ // Query represents a parsed TDQ query type Query struct { Root Node - Raw string // original query string - Sort *SortClause // optional sort clause + Raw string // original query string + Sort *SortClause // optional sort clause } func (q *Query) String() string { diff --git a/internal/query/execute.go b/internal/query/execute.go index 22258694..451a995c 100644 --- a/internal/query/execute.go +++ b/internal/query/execute.go @@ -125,7 +125,7 @@ func applyCrossEntityFilters(database QuerySource, issues []models.Issue, query // crossEntityPrefetch holds pre-fetched bulk data to avoid per-issue queries type crossEntityPrefetch struct { - reworkIDs map[string]bool + reworkIDs map[string]bool issuesWithOpenDeps map[string]bool } diff --git a/internal/query/execute_test.go b/internal/query/execute_test.go index 983ae0b5..d675b218 100644 --- a/internal/query/execute_test.go +++ b/internal/query/execute_test.go @@ -95,9 +95,9 @@ func TestExecute(t *testing.T) { wantCount: 2, }, { - name: "invalid query", - query: "status = ", - wantErr: true, + name: "invalid query", + query: "status = ", + wantErr: true, }, } diff --git a/internal/query/lexer_test.go b/internal/query/lexer_test.go index 4639333e..26bc8830 100644 --- a/internal/query/lexer_test.go +++ b/internal/query/lexer_test.go @@ -993,8 +993,8 @@ func TestLexerPositionTracking(t *testing.T) { line int column int }{ - {TokenIdent, 0, 1, 1}, // "status" - {TokenEq, 7, 1, 8}, // "=" + {TokenIdent, 0, 1, 1}, // "status" + {TokenEq, 7, 1, 8}, // "=" {TokenIdent, 10, 2, 2}, // "open" }, }, 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..fc53c227 100644 --- a/internal/serve/response_test.go +++ b/internal/serve/response_test.go @@ -127,7 +127,7 @@ func TestEnvelopeJSONShape(t *testing.T) { WriteSuccess(w, "hello", http.StatusOK) var raw map[string]interface{} - json.Unmarshal(w.Body.Bytes(), &raw) + must(t, json.Unmarshal(w.Body.Bytes(), &raw)) if _, exists := raw["error"]; exists { t.Error("success response should not have 'error' key") @@ -145,7 +145,7 @@ func TestEnvelopeJSONShape(t *testing.T) { WriteError(w, ErrInternal, "fail", http.StatusInternalServerError) var raw map[string]interface{} - json.Unmarshal(w.Body.Bytes(), &raw) + must(t, json.Unmarshal(w.Body.Bytes(), &raw)) if _, exists := raw["data"]; exists { t.Error("error response should not have 'data' key") @@ -329,7 +329,7 @@ func TestIssueToDTO_LabelsNeverNull(t *testing.T) { } var raw map[string]interface{} - json.Unmarshal(data, &raw) + must(t, json.Unmarshal(data, &raw)) labels, ok := raw["labels"].([]interface{}) if !ok { t.Fatalf("labels should be array, got %T (%v)", raw["labels"], raw["labels"]) @@ -357,7 +357,7 @@ func TestIssueToDTO_NullableFieldsSerializeAsNull(t *testing.T) { } var raw map[string]interface{} - json.Unmarshal(data, &raw) + must(t, json.Unmarshal(data, &raw)) nullFields := []string{"parent_id", "implementer_session", "creator_session", "reviewer_session", "created_branch", "defer_until", "due_date", "closed_at", "deleted_at"} @@ -472,7 +472,7 @@ func TestHandoffToDTO_EmptySlicesNotNull(t *testing.T) { // Verify JSON data, _ := json.Marshal(dto) var raw map[string]interface{} - json.Unmarshal(data, &raw) + must(t, json.Unmarshal(data, &raw)) for _, field := range []string{"done", "remaining", "decisions", "uncertain"} { arr, ok := raw[field].([]interface{}) diff --git a/internal/serve/session.go b/internal/serve/session.go index 1e1c02c4..4791bf42 100644 --- a/internal/serve/session.go +++ b/internal/serve/session.go @@ -9,11 +9,11 @@ import ( ) const ( - webAgentType = "web" - webAgentPID = 0 - webBranch = "default" - webSessionName = "td-serve-web" - heartbeatInterval = 60 * time.Second + webAgentType = "web" + webAgentPID = 0 + webBranch = "default" + webSessionName = "td-serve-web" + heartbeatInterval = 60 * time.Second ) // GetOrCreateWebSession finds or creates the shared web session used by 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/serve/test_helpers_test.go b/internal/serve/test_helpers_test.go new file mode 100644 index 00000000..463f0c03 --- /dev/null +++ b/internal/serve/test_helpers_test.go @@ -0,0 +1,10 @@ +package serve + +import "testing" + +func must(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatal(err) + } +} diff --git a/internal/serverdb/auth_events.go b/internal/serverdb/auth_events.go index 2db3a39a..3503daca 100644 --- a/internal/serverdb/auth_events.go +++ b/internal/serverdb/auth_events.go @@ -9,12 +9,12 @@ import ( // AuthEvent represents a row in the auth_events table. type AuthEvent struct { - ID int64 `json:"id"` - AuthRequestID string `json:"auth_request_id"` - Email string `json:"email"` - EventType string `json:"event_type"` - Metadata string `json:"metadata"` - CreatedAt string `json:"created_at"` + ID int64 `json:"id"` + AuthRequestID string `json:"auth_request_id"` + Email string `json:"email"` + EventType string `json:"event_type"` + Metadata string `json:"metadata"` + CreatedAt string `json:"created_at"` } // Auth event type constants. diff --git a/internal/session/agent_fingerprint_test.go b/internal/session/agent_fingerprint_test.go index 049cdd91..1b3fbf17 100644 --- a/internal/session/agent_fingerprint_test.go +++ b/internal/session/agent_fingerprint_test.go @@ -241,9 +241,9 @@ func TestExplicitIDOverridesAutoDetection(t *testing.T) { // TestMultipleExplicitIDValues verifies different fingerprints with different ExplicitIDs func TestMultipleExplicitIDValues(t *testing.T) { tests := []struct { - name string - sessionID1 string - sessionID2 string + name string + sessionID1 string + sessionID2 string shouldDiffer bool }{ { @@ -290,11 +290,11 @@ func TestMultipleExplicitIDValues(t *testing.T) { // TestEmptyVsPopulatedExplicitID verifies behavior with empty vs populated ExplicitID func TestEmptyVsPopulatedExplicitID(t *testing.T) { tests := []struct { - name string - explicit string - explicitType AgentType - pid int - expectedStr string + name string + explicit string + explicitType AgentType + pid int + expectedStr string }{ { name: "empty ExplicitID falls back to PID format", @@ -344,10 +344,10 @@ func TestEmptyVsPopulatedExplicitID(t *testing.T) { // TestExplicitIDWithSpecialCharacters verifies sanitization of special characters func TestExplicitIDWithSpecialCharacters(t *testing.T) { tests := []struct { - name string - sessionID string - expectedStr string - description string + name string + sessionID string + expectedStr string + description string }{ { name: "slashes converted to underscores", @@ -486,21 +486,21 @@ func TestExplicitIDTruncation(t *testing.T) { description string }{ { - name: "very long ID without special chars", - sessionID: "abcdefghijklmnopqrstuvwxyz0123456789", - maxLen: 32, + name: "very long ID without special chars", + sessionID: "abcdefghijklmnopqrstuvwxyz0123456789", + maxLen: 32, description: "long alphanumeric", }, { - name: "very long ID with special chars", - sessionID: "session-with-very-long-name-containing-special-chars-!@#$%^&*()", - maxLen: 32, + name: "very long ID with special chars", + sessionID: "session-with-very-long-name-containing-special-chars-!@#$%^&*()", + maxLen: 32, description: "long with special chars", }, { - name: "UUID-like long ID", - sessionID: "550e8400-e29b-41d4-a716-446655440000-extra-long-suffix", - maxLen: 32, + name: "UUID-like long ID", + sessionID: "550e8400-e29b-41d4-a716-446655440000-extra-long-suffix", + maxLen: 32, description: "long UUID", }, } @@ -529,18 +529,18 @@ func TestExplicitIDTruncation(t *testing.T) { // TestExplicitIDEnvironmentVarPriority verifies TD_SESSION_ID env var handling func TestExplicitIDEnvironmentVarPriority(t *testing.T) { tests := []struct { - name string - sessionID string + name string + sessionID string shouldHaveExplicit bool }{ { - name: "non-empty TD_SESSION_ID is used", - sessionID: "env-session-id", + name: "non-empty TD_SESSION_ID is used", + sessionID: "env-session-id", shouldHaveExplicit: true, }, { - name: "whitespace-only TD_SESSION_ID treated as empty", - sessionID: " ", + name: "whitespace-only TD_SESSION_ID treated as empty", + sessionID: " ", shouldHaveExplicit: false, }, } @@ -571,10 +571,10 @@ func TestExplicitIDEnvironmentVarPriority(t *testing.T) { // TestExplicitIDEdgeCases tests various edge cases for ExplicitID func TestExplicitIDEdgeCases(t *testing.T) { tests := []struct { - name string - sessionID string - pidValue int - typeValue AgentType + name string + sessionID string + pidValue int + typeValue AgentType expectedLen int description string }{ diff --git a/internal/session/session.go b/internal/session/session.go index 1e1fbbd5..4cae7bba 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -27,10 +27,10 @@ var getOrCreateMu sync.Mutex type Session struct { ID string `json:"id"` Name string `json:"name,omitempty"` - Branch string `json:"branch,omitempty"` // git branch for session scoping - AgentType string `json:"agent_type,omitempty"` // agent type (claude-code, cursor, terminal, etc.) - AgentPID int `json:"agent_pid,omitempty"` // stable parent agent process ID - ContextID string `json:"context_id,omitempty"` // audit only, not used for matching + Branch string `json:"branch,omitempty"` // git branch for session scoping + AgentType string `json:"agent_type,omitempty"` // agent type (claude-code, cursor, terminal, etc.) + AgentPID int `json:"agent_pid,omitempty"` // stable parent agent process ID + ContextID string `json:"context_id,omitempty"` // audit only, not used for matching PreviousSessionID string `json:"previous_session_id,omitempty"` StartedAt time.Time `json:"started_at"` LastActivity time.Time `json:"last_activity,omitempty"` // heartbeat for session liveness diff --git a/internal/sync/backfill_test.go b/internal/sync/backfill_test.go index 758abf49..a18b4895 100644 --- a/internal/sync/backfill_test.go +++ b/internal/sync/backfill_test.go @@ -249,14 +249,14 @@ func TestBackfillStaleIssues_AddsUpdate(t *testing.T) { if err != nil { t.Fatal(err) } - tx.Commit() + must(t, tx.Commit()) 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) + must(t, db.QueryRow(`SELECT COUNT(*) FROM action_log WHERE entity_id='td-700' AND action_type='create'`).Scan(&count)) if count != 2 { t.Fatalf("expected 2 create entries for td-700 (original + backfill), got %d", count) } @@ -275,7 +275,7 @@ func TestBackfillStaleIssues_SkipsWhenUpToDate(t *testing.T) { if err != nil { t.Fatal(err) } - tx.Commit() + must(t, tx.Commit()) if n != 0 { t.Fatalf("expected 0 stale updates, got %d", n) @@ -295,7 +295,7 @@ func TestBackfillStaleIssues_BackfillsInvalidJSON(t *testing.T) { if err != nil { t.Fatal(err) } - tx.Commit() + must(t, tx.Commit()) if n != 1 { t.Fatalf("expected 1 stale update for invalid JSON, got %d", n) @@ -314,7 +314,7 @@ func TestBackfillOrphanEntities_MultipleEntityTypes(t *testing.T) { if err != nil { t.Fatal(err) } - tx.Commit() + must(t, tx.Commit()) if n != 3 { t.Fatalf("expected 3 backfilled, got %d", n) @@ -400,7 +400,7 @@ func TestBackfillOrphanEntities_IncludesSoftDeleted(t *testing.T) { if err != nil { t.Fatal(err) } - tx.Commit() + must(t, tx.Commit()) if n != 1 { t.Fatalf("expected 1 backfilled (soft-deleted), got %d", n) @@ -430,7 +430,7 @@ func TestBackfillOrphanEntities_SkipsAfterPull(t *testing.T) { if err != nil { t.Fatal(err) } - tx.Commit() + must(t, tx.Commit()) if n != 0 { t.Fatalf("expected 0 backfilled after pull, got %d", n) @@ -438,7 +438,7 @@ 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) + must(t, db.QueryRow(`SELECT COUNT(*) FROM action_log`).Scan(&count)) 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..2bb33cfb 100644 --- a/internal/sync/client_test.go +++ b/internal/sync/client_test.go @@ -83,7 +83,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 +153,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 +176,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 +225,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 +260,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 +318,7 @@ func TestApplyRemoteEvents_Basic(t *testing.T) { if err != nil { t.Fatalf("ApplyRemoteEvents: %v", err) } - tx.Commit() + must(t, tx.Commit()) if result.Applied != 3 { t.Fatalf("Applied: got %d, want 3", result.Applied) @@ -379,7 +379,7 @@ func TestApplyRemoteEvents_PartialFailure(t *testing.T) { if err != nil { t.Fatalf("ApplyRemoteEvents: %v", err) } - tx.Commit() + must(t, tx.Commit()) if result.Applied != 2 { t.Fatalf("Applied: got %d, want 2", result.Applied) @@ -396,7 +396,7 @@ func TestApplyRemoteEvents_PartialFailure(t *testing.T) { // Verify good entities exist var count int - db.QueryRow("SELECT COUNT(*) FROM issues").Scan(&count) + must(t, db.QueryRow("SELECT COUNT(*) FROM issues").Scan(&count)) if count != 2 { t.Fatalf("issues count: got %d, want 2", count) } @@ -411,7 +411,7 @@ func TestApplyRemoteEvents_ConflictTracking(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p1); err != nil { t.Fatalf("seed: %v", err) } - tx.Commit() + must(t, tx.Commit()) // Apply remote event that overwrites remotePayload, _ := json.Marshal(map[string]any{ @@ -432,7 +432,7 @@ func TestApplyRemoteEvents_ConflictTracking(t *testing.T) { if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() + must(t, tx.Commit()) if result.Overwrites != 1 { t.Fatalf("expected 1 overwrite, got %d", result.Overwrites) @@ -479,7 +479,7 @@ func TestApplyRemoteEvents_MultipleOverwritesProduceConflicts(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i2", p2); err != nil { t.Fatalf("seed i2: %v", err) } - tx.Commit() + must(t, tx.Commit()) // Apply batch of remote events that overwrite both makePayload := func(title, status string) []byte { @@ -500,7 +500,7 @@ func TestApplyRemoteEvents_MultipleOverwritesProduceConflicts(t *testing.T) { if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() + must(t, tx.Commit()) if result.Applied != 2 { t.Fatalf("Applied=%d, want 2", result.Applied) @@ -534,7 +534,7 @@ func TestApplyRemoteEvents_DeleteDoesNotProduceConflict(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p); err != nil { t.Fatalf("seed: %v", err) } - tx.Commit() + must(t, tx.Commit()) // Apply a delete event from remote deletePayload, _ := json.Marshal(map[string]any{ @@ -550,7 +550,7 @@ func TestApplyRemoteEvents_DeleteDoesNotProduceConflict(t *testing.T) { if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() + must(t, tx.Commit()) if result.Applied != 1 { t.Fatalf("Applied=%d, want 1", result.Applied) @@ -564,7 +564,7 @@ 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) + must(t, db.QueryRow("SELECT COUNT(*) FROM issues WHERE id = ?", "i1").Scan(&count)) if count != 0 { t.Fatal("row should be deleted") } @@ -583,7 +583,7 @@ func TestApplyRemoteEvents_ConflictDataCorrectness(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", localFields); err != nil { t.Fatalf("seed: %v", err) } - tx.Commit() + must(t, tx.Commit()) // Remote overwrites with different data remoteFields := map[string]any{ @@ -605,7 +605,7 @@ func TestApplyRemoteEvents_ConflictDataCorrectness(t *testing.T) { if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() + must(t, tx.Commit()) if len(result.Conflicts) != 1 { t.Fatalf("expected 1 conflict, got %d", len(result.Conflicts)) @@ -660,7 +660,7 @@ func TestApplyRemoteEvents_NoConflictWhenUnchangedSinceSync(t *testing.T) { if err != nil { t.Fatalf("seed: %v", err) } - tx.Commit() + must(t, tx.Commit()) // 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 +679,7 @@ func TestApplyRemoteEvents_NoConflictWhenUnchangedSinceSync(t *testing.T) { if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() + must(t, tx.Commit()) if result.Applied != 1 { t.Fatalf("Applied=%d, want 1", result.Applied) @@ -703,7 +703,7 @@ func TestApplyRemoteEvents_ConflictWhenModifiedAfterSync(t *testing.T) { if err != nil { t.Fatalf("seed: %v", err) } - tx.Commit() + must(t, tx.Commit()) // 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 +722,7 @@ func TestApplyRemoteEvents_ConflictWhenModifiedAfterSync(t *testing.T) { if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() + must(t, tx.Commit()) if result.Overwrites != 1 { t.Fatalf("Overwrites=%d, want 1 (local was modified after sync)", result.Overwrites) @@ -741,7 +741,7 @@ func TestApplyRemoteEvents_NilLastSyncAtSkipsConflicts(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p); err != nil { t.Fatalf("seed: %v", err) } - tx.Commit() + must(t, tx.Commit()) // Apply remote overwrite with nil lastSyncAt (bootstrap scenario) remotePayload, _ := json.Marshal(map[string]any{ @@ -758,7 +758,7 @@ func TestApplyRemoteEvents_NilLastSyncAtSkipsConflicts(t *testing.T) { if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() + must(t, tx.Commit()) if result.Overwrites != 0 { t.Fatalf("Overwrites=%d, want 0 (nil lastSyncAt = no conflicts)", result.Overwrites) @@ -777,8 +777,8 @@ 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) + must(t, db.QueryRow("SELECT rowid FROM action_log WHERE id = ?", "al-00000001").Scan(&rowid1)) + must(t, db.QueryRow("SELECT rowid FROM action_log WHERE id = ?", "al-00000002").Scan(&rowid2)) acks := []Ack{ {ClientActionID: rowid1, ServerSeq: 100}, @@ -789,13 +789,13 @@ func TestMarkEventsSynced(t *testing.T) { if err := MarkEventsSynced(tx, acks); err != nil { t.Fatalf("MarkEventsSynced: %v", err) } - tx.Commit() + must(t, tx.Commit()) // 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) + must(t, db.QueryRow("SELECT synced_at, server_seq FROM action_log WHERE id = ?", "al-00000001").Scan(&syncedAt, &serverSeq)) if !syncedAt.Valid { t.Error("al-00000001: synced_at should be set") } @@ -803,7 +803,7 @@ 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) + must(t, db.QueryRow("SELECT synced_at, server_seq FROM action_log WHERE id = ?", "al-00000002").Scan(&syncedAt, &serverSeq)) if !syncedAt.Valid { t.Error("al-00000002: synced_at should be set") } @@ -812,7 +812,7 @@ 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) + must(t, db.QueryRow("SELECT synced_at, server_seq FROM action_log WHERE id = ?", "al-00000003").Scan(&syncedAt, &serverSeq)) if syncedAt.Valid { t.Error("al-00000003: synced_at should NOT be set") } @@ -822,7 +822,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 +853,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 +890,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..37b4a84f 100644 --- a/internal/sync/engine_test.go +++ b/internal/sync/engine_test.go @@ -48,7 +48,7 @@ func TestInsertServerEvents_Basic(t *testing.T) { if err != nil { t.Fatalf("insert: %v", err) } - tx.Commit() + must(t, tx.Commit()) if result.Accepted != 3 { t.Fatalf("accepted: got %d, want 3", result.Accepted) @@ -90,7 +90,7 @@ func TestInsertServerEvents_Dedup(t *testing.T) { if err != nil { t.Fatalf("first insert: %v", err) } - tx.Commit() + must(t, tx.Commit()) if r1.Accepted != 3 { t.Fatalf("first: accepted=%d, want 3", r1.Accepted) @@ -102,7 +102,7 @@ func TestInsertServerEvents_Dedup(t *testing.T) { if err != nil { t.Fatalf("second insert: %v", err) } - tx.Commit() + must(t, tx.Commit()) if r2.Accepted != 0 { t.Fatalf("second: accepted=%d, want 0", r2.Accepted) @@ -122,7 +122,7 @@ func TestInsertServerEvents_Dedup(t *testing.T) { // Verify total count in DB var count int - db.QueryRow("SELECT COUNT(*) FROM events").Scan(&count) + must(t, db.QueryRow("SELECT COUNT(*) FROM events").Scan(&count)) if count != 3 { t.Fatalf("total events: got %d, want 3", count) } @@ -149,7 +149,7 @@ func TestInsertServerEvents_ValidationReject(t *testing.T) { if err != nil { t.Fatalf("insert: %v", err) } - tx.Commit() + must(t, tx.Commit()) if result.Accepted != 0 { t.Fatalf("accepted: got %d, want 0", result.Accepted) @@ -185,14 +185,14 @@ func TestGetEventsSince_All(t *testing.T) { if _, err := InsertServerEvents(tx, events); err != nil { t.Fatalf("insert: %v", err) } - tx.Commit() + must(t, tx.Commit()) tx, _ = db.Begin() result, err := GetEventsSince(tx, 0, 100, "") if err != nil { t.Fatalf("get: %v", err) } - tx.Commit() + must(t, tx.Commit()) if len(result.Events) != 5 { t.Fatalf("events: got %d, want 5", len(result.Events)) @@ -216,14 +216,14 @@ func TestGetEventsSince_Partial(t *testing.T) { if _, err := InsertServerEvents(tx, events); err != nil { t.Fatalf("insert: %v", err) } - tx.Commit() + must(t, tx.Commit()) tx, _ = db.Begin() result, err := GetEventsSince(tx, 3, 100, "") if err != nil { t.Fatalf("get: %v", err) } - tx.Commit() + must(t, tx.Commit()) if len(result.Events) != 2 { t.Fatalf("events: got %d, want 2", len(result.Events)) @@ -247,14 +247,14 @@ func TestGetEventsSince_Limit(t *testing.T) { if _, err := InsertServerEvents(tx, events); err != nil { t.Fatalf("insert: %v", err) } - tx.Commit() + must(t, tx.Commit()) tx, _ = db.Begin() result, err := GetEventsSince(tx, 0, 3, "") if err != nil { t.Fatalf("get: %v", err) } - tx.Commit() + must(t, tx.Commit()) if len(result.Events) != 3 { t.Fatalf("events: got %d, want 3", len(result.Events)) @@ -277,14 +277,14 @@ func TestGetEventsSince_ExcludeDevice(t *testing.T) { if _, err := InsertServerEvents(tx, events); err != nil { t.Fatalf("insert: %v", err) } - tx.Commit() + must(t, tx.Commit()) tx, _ = db.Begin() result, err := GetEventsSince(tx, 0, 100, "d1") if err != nil { t.Fatalf("get: %v", err) } - tx.Commit() + must(t, tx.Commit()) if len(result.Events) != 2 { t.Fatalf("events: got %d, want 2", len(result.Events)) @@ -304,7 +304,7 @@ func TestGetEventsSince_Empty(t *testing.T) { if err != nil { t.Fatalf("get: %v", err) } - tx.Commit() + must(t, tx.Commit()) 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..2f88f86d 100644 --- a/internal/sync/events_test.go +++ b/internal/sync/events_test.go @@ -66,7 +66,7 @@ func TestUpsertEntity_Create(t *testing.T) { if err != nil { t.Fatalf("upsert: %v", err) } - tx.Commit() + must(t, tx.Commit()) var title, status string err = db.QueryRow("SELECT title, status FROM issues WHERE id = ?", "i1").Scan(&title, &status) @@ -87,7 +87,7 @@ func TestUpsertEntity_Update(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p1); err != nil { t.Fatalf("insert: %v", err) } - tx.Commit() + must(t, tx.Commit()) // Upsert with new title tx = beginTx(t, db) @@ -95,10 +95,10 @@ func TestUpsertEntity_Update(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p2); err != nil { t.Fatalf("upsert: %v", err) } - tx.Commit() + must(t, tx.Commit()) var title, status string - db.QueryRow("SELECT title, status FROM issues WHERE id = ?", "i1").Scan(&title, &status) + must(t, db.QueryRow("SELECT title, status FROM issues WHERE id = ?", "i1").Scan(&title, &status)) if title != "new" || status != "closed" { t.Fatalf("got title=%q status=%q", title, status) } @@ -113,7 +113,7 @@ func TestUpsertExistingEntity(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p1); err != nil { t.Fatalf("create: %v", err) } - tx.Commit() + must(t, tx.Commit()) // Upsert with completely different data tx = beginTx(t, db) @@ -121,12 +121,12 @@ func TestUpsertExistingEntity(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p2); err != nil { t.Fatalf("upsert: %v", err) } - tx.Commit() + must(t, tx.Commit()) 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) + must(t, db.QueryRow("SELECT title, status, priority FROM issues WHERE id = ?", "i1").Scan(&title, &status, &priority)) if title != "replaced" { t.Fatalf("title should be replaced, got %q", title) } @@ -148,7 +148,7 @@ func TestPartialPayloadDropsColumns(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p1); err != nil { t.Fatalf("create: %v", err) } - tx.Commit() + must(t, tx.Commit()) // Upsert with only title tx = beginTx(t, db) @@ -156,11 +156,11 @@ func TestPartialPayloadDropsColumns(t *testing.T) { if _, err := upsertEntity(tx, "issues", "i1", p2); err != nil { t.Fatalf("upsert: %v", err) } - tx.Commit() + must(t, tx.Commit()) var title string var status, priority sql.NullString - db.QueryRow("SELECT title, status, priority FROM issues WHERE id = ?", "i1").Scan(&title, &status, &priority) + must(t, db.QueryRow("SELECT title, status, priority FROM issues WHERE id = ?", "i1").Scan(&title, &status, &priority)) if title != "partial" { t.Fatalf("title should be partial, got %q", title) } @@ -175,7 +175,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 +191,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 +207,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 +234,7 @@ func TestUpdateDoesNotRecreateAfterDelete(t *testing.T) { if err != nil { t.Fatalf("create: %v", err) } - tx.Commit() + must(t, tx.Commit()) // Delete tx = beginTx(t, db) @@ -247,7 +247,7 @@ func TestUpdateDoesNotRecreateAfterDelete(t *testing.T) { if err != nil { t.Fatalf("delete: %v", err) } - tx.Commit() + must(t, tx.Commit()) // Update after delete should be ignored tx = beginTx(t, db) @@ -260,10 +260,10 @@ func TestUpdateDoesNotRecreateAfterDelete(t *testing.T) { if err != nil { t.Fatalf("update: %v", err) } - tx.Commit() + must(t, tx.Commit()) var count int - db.QueryRow("SELECT COUNT(*) FROM issues WHERE id = ?", "i1").Scan(&count) + must(t, db.QueryRow("SELECT COUNT(*) FROM issues WHERE id = ?", "i1").Scan(&count)) if count != 0 { t.Fatalf("expected issue to remain deleted, got count=%d", count) } @@ -272,7 +272,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 +288,7 @@ func TestColumnNameInjection_DroppedSilently(t *testing.T) { // Verify the table wasn't dropped var count int - db.QueryRow("SELECT COUNT(*) FROM issues").Scan(&count) + must(t, tx.QueryRow("SELECT COUNT(*) FROM issues").Scan(&count)) if count != 0 { t.Fatalf("expected 0 rows, got %d", count) } @@ -299,16 +299,16 @@ func TestDeleteEntity(t *testing.T) { tx := beginTx(t, db) p, _ := json.Marshal(map[string]any{"title": "bye"}) _, _ = upsertEntity(tx, "issues", "i1", p) - tx.Commit() + must(t, tx.Commit()) tx = beginTx(t, db) if err := deleteEntity(tx, "issues", "i1"); err != nil { t.Fatalf("delete: %v", err) } - tx.Commit() + must(t, tx.Commit()) var count int - db.QueryRow("SELECT COUNT(*) FROM issues WHERE id = ?", "i1").Scan(&count) + must(t, db.QueryRow("SELECT COUNT(*) FROM issues WHERE id = ?", "i1").Scan(&count)) if count != 0 { t.Fatalf("expected 0 rows, got %d", count) } @@ -320,7 +320,7 @@ 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() + must(t, tx.Commit()) } func TestSoftDeleteEntity(t *testing.T) { @@ -328,17 +328,17 @@ func TestSoftDeleteEntity(t *testing.T) { tx := beginTx(t, db) p, _ := json.Marshal(map[string]any{"title": "soft"}) _, _ = upsertEntity(tx, "issues", "i1", p) - tx.Commit() + must(t, tx.Commit()) 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() + must(t, tx.Commit()) var deletedAt sql.NullTime - db.QueryRow("SELECT deleted_at FROM issues WHERE id = ?", "i1").Scan(&deletedAt) + must(t, db.QueryRow("SELECT deleted_at FROM issues WHERE id = ?", "i1").Scan(&deletedAt)) if !deletedAt.Valid { t.Fatal("deleted_at should be set") } @@ -350,13 +350,13 @@ 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() + must(t, tx.Commit()) } 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 +371,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 +398,10 @@ func TestApplyEvent_Create(t *testing.T) { if err != nil { t.Fatalf("apply create: %v", err) } - tx.Commit() + must(t, tx.Commit()) var title string - db.QueryRow("SELECT title FROM issues WHERE id = ?", "i1").Scan(&title) + must(t, db.QueryRow("SELECT title FROM issues WHERE id = ?", "i1").Scan(&title)) if title != "via apply" { t.Fatalf("got title=%q", title) } @@ -414,7 +414,7 @@ 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() + must(t, tx.Commit()) // Update tx = beginTx(t, db) @@ -423,10 +423,10 @@ func TestApplyEvent_Update(t *testing.T) { if err != nil { t.Fatalf("apply update: %v", err) } - tx.Commit() + must(t, tx.Commit()) var title, status string - db.QueryRow("SELECT title, status FROM issues WHERE id = ?", "i1").Scan(&title, &status) + must(t, db.QueryRow("SELECT title, status FROM issues WHERE id = ?", "i1").Scan(&title, &status)) if title != "updated" || status != "closed" { t.Fatalf("got title=%q status=%q", title, status) } @@ -448,7 +448,7 @@ func TestUpsertEntity_OverwriteDetection(t *testing.T) { if res.OldData != nil { t.Fatal("first insert should have nil OldData") } - tx.Commit() + must(t, tx.Commit()) // Second insert to same ID should be an overwrite tx = beginTx(t, db) @@ -471,7 +471,7 @@ func TestUpsertEntity_OverwriteDetection(t *testing.T) { if old["title"] != "first" { t.Fatalf("OldData title=%v, want 'first'", old["title"]) } - tx.Commit() + must(t, tx.Commit()) // Insert to different ID should not be an overwrite tx = beginTx(t, db) @@ -483,7 +483,7 @@ func TestUpsertEntity_OverwriteDetection(t *testing.T) { if res.Overwritten { t.Fatal("insert to new ID should not be an overwrite") } - tx.Commit() + must(t, tx.Commit()) } func TestApplyEvent_OverwriteTracking(t *testing.T) { @@ -499,7 +499,7 @@ func TestApplyEvent_OverwriteTracking(t *testing.T) { if overwritten { t.Fatal("create should not report overwrite") } - tx.Commit() + must(t, tx.Commit()) // Update same entity tx = beginTx(t, db) @@ -511,7 +511,7 @@ func TestApplyEvent_OverwriteTracking(t *testing.T) { if !overwritten { t.Fatal("update to existing entity should report overwrite") } - tx.Commit() + must(t, tx.Commit()) } func TestUpsertEntity_LabelsArrayNormalized(t *testing.T) { @@ -524,10 +524,10 @@ func TestUpsertEntity_LabelsArrayNormalized(t *testing.T) { if err != nil { t.Fatalf("upsert with labels array: %v", err) } - tx.Commit() + must(t, tx.Commit()) var labels string - db.QueryRow("SELECT labels FROM issues WHERE id = ?", "i1").Scan(&labels) + must(t, db.QueryRow("SELECT labels FROM issues WHERE id = ?", "i1").Scan(&labels)) if labels != "bug,urgent" { t.Fatalf("labels: got %q, want 'bug,urgent'", labels) } @@ -542,11 +542,11 @@ func TestUpsertEntity_HandoffArraysNormalized(t *testing.T) { if err != nil { t.Fatalf("upsert handoff with arrays: %v", err) } - tx.Commit() + must(t, tx.Commit()) var done, remaining, decisions, uncertain string - db.QueryRow("SELECT done, remaining, decisions, uncertain FROM handoffs WHERE id = ?", "h1"). - Scan(&done, &remaining, &decisions, &uncertain) + must(t, db.QueryRow("SELECT done, remaining, decisions, uncertain FROM handoffs WHERE id = ?", "h1"). + Scan(&done, &remaining, &decisions, &uncertain)) if done != `["task A"]` { t.Fatalf("done: got %q, want '[\"task A\"]'", done) @@ -572,10 +572,10 @@ func TestUpsertEntity_NestedObjectNormalized(t *testing.T) { if err != nil { t.Fatalf("upsert with nested object: %v", err) } - tx.Commit() + must(t, tx.Commit()) var priority string - db.QueryRow("SELECT priority FROM issues WHERE id = ?", "i1").Scan(&priority) + must(t, db.QueryRow("SELECT priority FROM issues WHERE id = ?", "i1").Scan(&priority)) if priority != `{"level":"high","score":5}` { t.Fatalf("priority: got %q", priority) } @@ -584,7 +584,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 +611,7 @@ func TestUpsertEntity_UnknownFieldsIgnored(t *testing.T) { if err != nil { t.Fatalf("upsert with unknown fields: %v", err) } - tx.Commit() + must(t, tx.Commit()) var title, status string err = db.QueryRow("SELECT title, status FROM issues WHERE id = ?", "i1").Scan(&title, &status) @@ -626,7 +626,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 +688,7 @@ func TestApplyEvent_DeferFields(t *testing.T) { if err != nil { t.Fatalf("apply create: %v", err) } - tx.Commit() + must(t, tx.Commit()) // Verify all fields persisted var title, deferUntil, dueDate sql.NullString @@ -816,7 +816,7 @@ func TestApplyEvent_DeferFieldsPartialUpdate(t *testing.T) { if err != nil { t.Fatalf("create: %v", err) } - tx.Commit() + must(t, tx.Commit()) // Partial update: change only defer_until via applyEventWithPrevious previousData, _ := json.Marshal(map[string]any{ @@ -941,7 +941,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 +958,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 +975,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 +996,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 +1012,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 +1034,7 @@ 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) + must(t, tx.QueryRow("SELECT COUNT(*) FROM issue_dependencies WHERE issue_id='B' AND depends_on_id='A'").Scan(&count)) if count != 0 { t.Fatalf("B->A should have been removed, got count=%d", count) } @@ -1043,7 +1043,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 +1065,7 @@ 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) + must(t, tx.QueryRow("SELECT COUNT(*) FROM issue_dependencies WHERE issue_id='A' AND depends_on_id='B'").Scan(&count)) if count != 1 { t.Fatalf("A->B should still exist, got count=%d", count) } diff --git a/internal/sync/test_helpers_test.go b/internal/sync/test_helpers_test.go new file mode 100644 index 00000000..7e642797 --- /dev/null +++ b/internal/sync/test_helpers_test.go @@ -0,0 +1,10 @@ +package sync + +import "testing" + +func must(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatal(err) + } +} diff --git a/internal/syncclient/client.go b/internal/syncclient/client.go index 2bc548b2..22d7ac9e 100644 --- a/internal/syncclient/client.go +++ b/internal/syncclient/client.go @@ -264,8 +264,8 @@ func (c *Client) Pull(projectID string, afterSeq int64, limit int, excludeDevice // SnapshotResponse holds the result of a snapshot download. type SnapshotResponse struct { - Data []byte - SnapshotSeq int64 + Data []byte + SnapshotSeq int64 } // GetSnapshot downloads a snapshot database for bootstrap. diff --git a/internal/version/semver_test.go b/internal/version/semver_test.go index 9732795b..fc9cd5a6 100644 --- a/internal/version/semver_test.go +++ b/internal/version/semver_test.go @@ -90,7 +90,7 @@ func TestIsNewer(t *testing.T) { // Prerelease handling (same core version, ignoring prerelease) // When core versions are the same, neither is "newer" {"v1.0.0-beta", "v1.0.0", false}, // prerelease vs final (same core) - {"v1.0.0", "v1.0.0-beta", false}, // final vs prerelease (same core - not newer) + {"v1.0.0", "v1.0.0-beta", false}, // final vs prerelease (same core - not newer) {"v2.0.0-rc.1", "v1.9.9", true}, // Build metadata handling (build metadata ignored) @@ -170,9 +170,9 @@ func TestParseSemverEdgeCases(t *testing.T) { // TestIsNewerSymmetry tests that isNewer maintains logical consistency func TestIsNewerSymmetry(t *testing.T) { tests := []struct { - name string - v1 string - v2 string + name string + v1 string + v2 string }{ {"major-diff", "v2.0.0", "v1.0.0"}, {"minor-diff", "v1.5.0", "v1.0.0"}, diff --git a/internal/version/version_test.go b/internal/version/version_test.go index 06158057..7ff2f64d 100644 --- a/internal/version/version_test.go +++ b/internal/version/version_test.go @@ -37,7 +37,7 @@ func TestIsDevelopmentVersion(t *testing.T) { {"devel", true}, // Case sensitivity - {"DEV", false}, // case-sensitive, so should be false + {"DEV", false}, // case-sensitive, so should be false {"DEVEL", false}, {"Dev", false}, @@ -94,13 +94,13 @@ func TestUpdateCommand(t *testing.T) { {"../../.env", ""}, // Invalid: prerelease identifier errors - {"v1.2.3--", ""}, // double hyphen - {"v1.2.3-", ""}, // trailing hyphen - {"v1.2.3-beta-", ""}, // trailing hyphen in prerelease - {"v1.2.3-.beta", ""}, // leading dot after hyphen - {"v1.2.3-beta.", ""}, // trailing dot - {"v1.2.3-beta..rc", ""}, // double dot - {"v1.2.3-_invalid", ""}, // underscore in prerelease + {"v1.2.3--", ""}, // double hyphen + {"v1.2.3-", ""}, // trailing hyphen + {"v1.2.3-beta-", ""}, // trailing hyphen in prerelease + {"v1.2.3-.beta", ""}, // leading dot after hyphen + {"v1.2.3-beta.", ""}, // trailing dot + {"v1.2.3-beta..rc", ""}, // double dot + {"v1.2.3-_invalid", ""}, // underscore in prerelease {"v1.2.3-beta_release", ""}, // Invalid: missing version parts diff --git a/internal/workdir/associations_test.go b/internal/workdir/associations_test.go index 3f304369..31a1f137 100644 --- a/internal/workdir/associations_test.go +++ b/internal/workdir/associations_test.go @@ -26,8 +26,8 @@ func TestLoadSaveRoundTrip(t *testing.T) { t.Setenv("HOME", tmp) input := map[string]string{ - "/Users/alice/code/repo-one": "/Users/alice/notes/vault-one", - "/Users/alice/code/repo-two": "/Users/alice/notes/vault-two", + "/Users/alice/code/repo-one": "/Users/alice/notes/vault-one", + "/Users/alice/code/repo-two": "/Users/alice/notes/vault-two", } if err := SaveAssociations(input); err != nil { diff --git a/pkg/monitor/clipboard_test.go b/pkg/monitor/clipboard_test.go index 59a92552..b402b82f 100644 --- a/pkg/monitor/clipboard_test.go +++ b/pkg/monitor/clipboard_test.go @@ -338,8 +338,8 @@ func TestStatusIcon(t *testing.T) { // TestFormatIssueAsMarkdownEdgeCases tests edge cases in formatting func TestFormatIssueAsMarkdownEdgeCases(t *testing.T) { tests := []struct { - name string - issue *models.Issue + name string + issue *models.Issue validates func(string) error }{ { diff --git a/pkg/monitor/data_test.go b/pkg/monitor/data_test.go index 0b372a99..a8995bcd 100644 --- a/pkg/monitor/data_test.go +++ b/pkg/monitor/data_test.go @@ -160,7 +160,7 @@ func TestGetSortFuncWithPosition(t *testing.T) { name: "mixed - positioned come before unpositioned", sortMode: SortByPriority, issues: []models.BoardIssueView{ - {Issue: models.Issue{ID: "unpos-p0", Priority: models.PriorityP0, UpdatedAt: now}}, // high priority but unpositioned + {Issue: models.Issue{ID: "unpos-p0", Priority: models.PriorityP0, UpdatedAt: now}}, // high priority but unpositioned {Issue: models.Issue{ID: "pos-p3", Priority: models.PriorityP3}, Position: 1, HasPosition: true}, // low priority but positioned {Issue: models.Issue{ID: "unpos-p1", Priority: models.PriorityP1, UpdatedAt: now}}, }, diff --git a/pkg/monitor/input.go b/pkg/monitor/input.go index e55be125..7abdab3f 100644 --- a/pkg/monitor/input.go +++ b/pkg/monitor/input.go @@ -1554,4 +1554,3 @@ func (m Model) handleFormDialogHover(x, y int) (tea.Model, tea.Cmd) { return m, nil } - diff --git a/pkg/monitor/input_test.go b/pkg/monitor/input_test.go index 8f8da688..dc99ce5c 100644 --- a/pkg/monitor/input_test.go +++ b/pkg/monitor/input_test.go @@ -109,51 +109,51 @@ func TestRectContains(t *testing.T) { expected bool }{ { - name: "point inside rectangle", - rect: Rect{X: 10, Y: 20, W: 30, H: 40}, - x: 20, y: 30, + name: "point inside rectangle", + rect: Rect{X: 10, Y: 20, W: 30, H: 40}, + x: 20, y: 30, expected: true, }, { - name: "point at left boundary (inclusive)", - rect: Rect{X: 10, Y: 20, W: 30, H: 40}, - x: 10, y: 30, + name: "point at left boundary (inclusive)", + rect: Rect{X: 10, Y: 20, W: 30, H: 40}, + x: 10, y: 30, expected: true, }, { - name: "point at top boundary (inclusive)", - rect: Rect{X: 10, Y: 20, W: 30, H: 40}, - x: 20, y: 20, + name: "point at top boundary (inclusive)", + rect: Rect{X: 10, Y: 20, W: 30, H: 40}, + x: 20, y: 20, expected: true, }, { - name: "point at right boundary (exclusive)", - rect: Rect{X: 10, Y: 20, W: 30, H: 40}, - x: 40, y: 30, + name: "point at right boundary (exclusive)", + rect: Rect{X: 10, Y: 20, W: 30, H: 40}, + x: 40, y: 30, expected: false, }, { - name: "point at bottom boundary (exclusive)", - rect: Rect{X: 10, Y: 20, W: 30, H: 40}, - x: 20, y: 60, + name: "point at bottom boundary (exclusive)", + rect: Rect{X: 10, Y: 20, W: 30, H: 40}, + x: 20, y: 60, expected: false, }, { - name: "point outside left", - rect: Rect{X: 10, Y: 20, W: 30, H: 40}, - x: 9, y: 30, + name: "point outside left", + rect: Rect{X: 10, Y: 20, W: 30, H: 40}, + x: 9, y: 30, expected: false, }, { - name: "point outside top", - rect: Rect{X: 10, Y: 20, W: 30, H: 40}, - x: 20, y: 19, + name: "point outside top", + rect: Rect{X: 10, Y: 20, W: 30, H: 40}, + x: 20, y: 19, expected: false, }, { - name: "zero-sized rectangle", - rect: Rect{X: 10, Y: 20, W: 0, H: 0}, - x: 10, y: 20, + name: "zero-sized rectangle", + rect: Rect{X: 10, Y: 20, W: 0, H: 0}, + x: 10, y: 20, expected: false, }, } @@ -257,8 +257,8 @@ func TestHitTestRow_EmptyPanel(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := Model{ - PanelBounds: map[Panel]Rect{tt.panel: {X: 0, Y: 0, W: 100, H: 20}}, - ScrollOffset: map[Panel]int{tt.panel: 0}, + PanelBounds: map[Panel]Rect{tt.panel: {X: 0, Y: 0, W: 100, H: 20}}, + ScrollOffset: map[Panel]int{tt.panel: 0}, CurrentWorkRows: []string{}, TaskListRows: []TaskListRow{}, Activity: []ActivityItem{}, @@ -379,8 +379,8 @@ func TestHandleMouseWheel(t *testing.T) { description string }{ { - name: "scroll down within bounds", - x: 50, y: 15, + name: "scroll down within bounds", + x: 50, y: 15, delta: 3, initialOffset: 0, rowCount: 20, @@ -388,8 +388,8 @@ func TestHandleMouseWheel(t *testing.T) { description: "scrolling down by 3", }, { - name: "scroll up from offset", - x: 50, y: 15, + name: "scroll up from offset", + x: 50, y: 15, delta: -3, initialOffset: 5, rowCount: 20, @@ -397,8 +397,8 @@ func TestHandleMouseWheel(t *testing.T) { description: "scrolling up by 3", }, { - name: "scroll up clamps at 0", - x: 50, y: 15, + name: "scroll up clamps at 0", + x: 50, y: 15, delta: -5, initialOffset: 2, rowCount: 20, @@ -406,8 +406,8 @@ func TestHandleMouseWheel(t *testing.T) { description: "scrolling up past top clamps to 0", }, { - name: "scroll outside panel", - x: 200, y: 15, + name: "scroll outside panel", + x: 200, y: 15, delta: 3, initialOffset: 0, rowCount: 20, @@ -419,14 +419,14 @@ func TestHandleMouseWheel(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := Model{ - Height: 30, - Width: 100, - ActivePanel: PanelTaskList, - PaneHeights: config.DefaultPaneHeights(), - PanelBounds: map[Panel]Rect{PanelTaskList: {X: 0, Y: 10, W: 100, H: 15}}, - ScrollOffset: map[Panel]int{PanelTaskList: tt.initialOffset}, + Height: 30, + Width: 100, + ActivePanel: PanelTaskList, + PaneHeights: config.DefaultPaneHeights(), + PanelBounds: map[Panel]Rect{PanelTaskList: {X: 0, Y: 10, W: 100, H: 15}}, + ScrollOffset: map[Panel]int{PanelTaskList: tt.initialOffset}, ScrollIndependent: map[Panel]bool{PanelTaskList: false}, - TaskListRows: make([]TaskListRow, tt.rowCount), + TaskListRows: make([]TaskListRow, tt.rowCount), } updated, _ := m.handleMouseWheel(tt.x, tt.y, tt.delta) @@ -444,18 +444,18 @@ func TestHandleMouseWheel(t *testing.T) { // TestHandleMouseClick_ActivatesPanel tests panel activation on click func TestHandleMouseClick_ActivatesPanel(t *testing.T) { tests := []struct { - name string - x, y int - initialActive Panel - clickBounds map[Panel]Rect - expectedActive Panel - expectedRow int - description string + name string + x, y int + initialActive Panel + clickBounds map[Panel]Rect + expectedActive Panel + expectedRow int + description string }{ { - name: "click on different panel activates it", - x: 50, y: 15, - initialActive: PanelCurrentWork, + name: "click on different panel activates it", + x: 50, y: 15, + initialActive: PanelCurrentWork, clickBounds: map[Panel]Rect{ PanelCurrentWork: {X: 0, Y: 0, W: 100, H: 10}, PanelTaskList: {X: 0, Y: 10, W: 100, H: 10}, @@ -466,9 +466,9 @@ func TestHandleMouseClick_ActivatesPanel(t *testing.T) { description: "clicking TaskList activates it", }, { - name: "click on active panel keeps focus", - x: 50, y: 5, - initialActive: PanelCurrentWork, + name: "click on active panel keeps focus", + x: 50, y: 5, + initialActive: PanelCurrentWork, clickBounds: map[Panel]Rect{ PanelCurrentWork: {X: 0, Y: 0, W: 100, H: 10}, PanelTaskList: {X: 0, Y: 10, W: 100, H: 10}, @@ -511,49 +511,49 @@ func TestHandleMouseClick_DoubleClick(t *testing.T) { now := time.Now() tests := []struct { - name string - x, y int - lastClickTime time.Time - lastClickPanel Panel - lastClickRow int + name string + x, y int + lastClickTime time.Time + lastClickPanel Panel + lastClickRow int expectedDoubleClick bool - description string + description string }{ { - name: "same panel/row within 400ms is double-click", - x: 50, y: 15, - lastClickTime: now.Add(-100 * time.Millisecond), - lastClickPanel: PanelTaskList, - lastClickRow: 1, + name: "same panel/row within 400ms is double-click", + x: 50, y: 15, + lastClickTime: now.Add(-100 * time.Millisecond), + lastClickPanel: PanelTaskList, + lastClickRow: 1, expectedDoubleClick: true, - description: "double-click detected", + description: "double-click detected", }, { - name: "different row is not double-click", - x: 50, y: 16, - lastClickTime: now.Add(-100 * time.Millisecond), - lastClickPanel: PanelTaskList, - lastClickRow: 5, // Previous click was on row 5, current click assumed on row 1 + name: "different row is not double-click", + x: 50, y: 16, + lastClickTime: now.Add(-100 * time.Millisecond), + lastClickPanel: PanelTaskList, + lastClickRow: 5, // Previous click was on row 5, current click assumed on row 1 expectedDoubleClick: false, - description: "different row, not double-click", + description: "different row, not double-click", }, { - name: "different panel is not double-click", - x: 50, y: 15, - lastClickTime: now.Add(-100 * time.Millisecond), - lastClickPanel: PanelCurrentWork, - lastClickRow: 1, + name: "different panel is not double-click", + x: 50, y: 15, + lastClickTime: now.Add(-100 * time.Millisecond), + lastClickPanel: PanelCurrentWork, + lastClickRow: 1, expectedDoubleClick: false, - description: "different panel, not double-click", + description: "different panel, not double-click", }, { - name: "timeout > 400ms is not double-click", - x: 50, y: 15, - lastClickTime: now.Add(-500 * time.Millisecond), - lastClickPanel: PanelTaskList, - lastClickRow: 1, + name: "timeout > 400ms is not double-click", + x: 50, y: 15, + lastClickTime: now.Add(-500 * time.Millisecond), + lastClickPanel: PanelTaskList, + lastClickRow: 1, expectedDoubleClick: false, - description: "timeout exceeded, not double-click", + description: "timeout exceeded, not double-click", }, } @@ -570,9 +570,9 @@ func TestHandleMouseClick_DoubleClick(t *testing.T) { {Issue: models.Issue{ID: "t1"}}, {Issue: models.Issue{ID: "t2"}}, }, - LastClickTime: tt.lastClickTime, - LastClickPanel: tt.lastClickPanel, - LastClickRow: tt.lastClickRow, + LastClickTime: tt.lastClickTime, + LastClickPanel: tt.lastClickPanel, + LastClickRow: tt.lastClickRow, } // Simulate time passage @@ -599,7 +599,7 @@ func TestHandleMouseClick_DoubleClick(t *testing.T) { // TestStartDividerDrag tests beginning of divider drag operation func TestStartDividerDrag(t *testing.T) { m := Model{ - PaneHeights: [3]float64{0.3, 0.3, 0.4}, + PaneHeights: [3]float64{0.3, 0.3, 0.4}, DraggingDivider: -1, DragStartY: 0, } @@ -699,45 +699,45 @@ func TestEndDividerDrag(t *testing.T) { // TestHandleMouseMsg_WheelScroll tests mouse wheel scroll message handling func TestHandleMouseMsg_WheelScroll(t *testing.T) { tests := []struct { - name string - button tea.MouseButton - action tea.MouseAction + name string + button tea.MouseButton + action tea.MouseAction expectedScrollDelta int - description string + description string }{ { - name: "wheel up scrolls up", - button: tea.MouseButtonWheelUp, - action: tea.MouseActionPress, + name: "wheel up scrolls up", + button: tea.MouseButtonWheelUp, + action: tea.MouseActionPress, expectedScrollDelta: -3, - description: "scroll up by 3", + description: "scroll up by 3", }, { - name: "wheel down scrolls down", - button: tea.MouseButtonWheelDown, - action: tea.MouseActionPress, + name: "wheel down scrolls down", + button: tea.MouseButtonWheelDown, + action: tea.MouseActionPress, expectedScrollDelta: 3, - description: "scroll down by 3", + description: "scroll down by 3", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := Model{ - Height: 30, - Width: 100, - ActivePanel: PanelTaskList, - PaneHeights: config.DefaultPaneHeights(), - PanelBounds: map[Panel]Rect{PanelTaskList: {X: 0, Y: 10, W: 100, H: 15}}, - ScrollOffset: map[Panel]int{PanelTaskList: 0}, + Height: 30, + Width: 100, + ActivePanel: PanelTaskList, + PaneHeights: config.DefaultPaneHeights(), + PanelBounds: map[Panel]Rect{PanelTaskList: {X: 0, Y: 10, W: 100, H: 15}}, + ScrollOffset: map[Panel]int{PanelTaskList: 0}, ScrollIndependent: map[Panel]bool{PanelTaskList: false}, - TaskListRows: make([]TaskListRow, 20), - ModalStack: []ModalEntry{}, - StatsOpen: false, - HandoffsOpen: false, - ConfirmOpen: false, - HelpOpen: false, - ShowTDQHelp: false, + TaskListRows: make([]TaskListRow, 20), + ModalStack: []ModalEntry{}, + StatsOpen: false, + HandoffsOpen: false, + ConfirmOpen: false, + HelpOpen: false, + ShowTDQHelp: false, } msg := tea.MouseMsg{ @@ -809,9 +809,9 @@ func TestMouseCoordinateConversion(t *testing.T) { taskListBounds := m.PanelBounds[PanelTaskList] tests := []struct { - name string - absX int - absY int + name string + absX int + absY int expectedRelY int }{ { @@ -841,14 +841,14 @@ func TestMouseCoordinateConversion(t *testing.T) { // TestMouseClickWithScrolling tests mouse clicks while panel is scrolled func TestMouseClickWithScrolling(t *testing.T) { m := Model{ - Height: 30, - Width: 100, - ActivePanel: PanelTaskList, - PaneHeights: config.DefaultPaneHeights(), - PanelBounds: map[Panel]Rect{PanelTaskList: {X: 0, Y: 10, W: 100, H: 15}}, - Cursor: map[Panel]int{PanelTaskList: 0}, - SelectedID: map[Panel]string{}, - ScrollOffset: map[Panel]int{PanelTaskList: 5}, // Scrolled down + Height: 30, + Width: 100, + ActivePanel: PanelTaskList, + PaneHeights: config.DefaultPaneHeights(), + PanelBounds: map[Panel]Rect{PanelTaskList: {X: 0, Y: 10, W: 100, H: 15}}, + Cursor: map[Panel]int{PanelTaskList: 0}, + SelectedID: map[Panel]string{}, + ScrollOffset: map[Panel]int{PanelTaskList: 5}, // Scrolled down ScrollIndependent: map[Panel]bool{}, TaskListRows: []TaskListRow{ {Issue: models.Issue{ID: "t1"}}, @@ -887,35 +887,35 @@ func TestMouseClickWithScrolling(t *testing.T) { // TestUpdatePanelBounds tests panel bounds recalculation on window resize func TestUpdatePanelBounds(t *testing.T) { tests := []struct { - name string - width int - height int - searchMode bool - embedded bool + name string + width int + height int + searchMode bool + embedded bool expectedHeightSum int }{ { - name: "normal 3-panel layout", - width: 100, - height: 30, - searchMode: false, - embedded: false, + name: "normal 3-panel layout", + width: 100, + height: 30, + searchMode: false, + embedded: false, expectedHeightSum: 24, // 30 - 3 (footer) - 3 borders/titles }, { - name: "with search bar", - width: 100, - height: 30, - searchMode: true, - embedded: false, + name: "with search bar", + width: 100, + height: 30, + searchMode: true, + embedded: false, expectedHeightSum: 22, // 30 - 2 (search) - 3 (footer) - 3 borders/titles }, { - name: "embedded mode (no footer)", - width: 100, - height: 30, - searchMode: false, - embedded: true, + name: "embedded mode (no footer)", + width: 100, + height: 30, + searchMode: false, + embedded: true, expectedHeightSum: 27, // 30 - 3 borders/titles }, } @@ -1007,11 +1007,11 @@ func TestMaxScrollOffsetActivityPanel(t *testing.T) { // TestConfirmDialogButtonNavigation tests Tab navigation in delete confirmation dialog func TestConfirmDialogButtonNavigation(t *testing.T) { tests := []struct { - name string - initialFocus int - key string - expectedFocus int - description string + name string + initialFocus int + key string + expectedFocus int + description string }{ { name: "tab from yes to no", @@ -1069,11 +1069,11 @@ func TestConfirmDialogButtonNavigation(t *testing.T) { // TestCloseConfirmDialogButtonNavigation tests Tab navigation in close confirmation dialog func TestCloseConfirmDialogButtonNavigation(t *testing.T) { tests := []struct { - name string - initialFocus int - key string - expectedFocus int - description string + name string + initialFocus int + key string + expectedFocus int + description string }{ { name: "tab from input to confirm", @@ -1213,8 +1213,8 @@ func TestMouseClickEdgeCases(t *testing.T) { description string }{ { - name: "click at exact boundary", - x: 0, y: 0, + name: "click at exact boundary", + x: 0, y: 0, panelBounds: map[Panel]Rect{ PanelCurrentWork: {X: 0, Y: 0, W: 100, H: 10}, }, @@ -1222,8 +1222,8 @@ func TestMouseClickEdgeCases(t *testing.T) { description: "click at exact (0,0)", }, { - name: "click at negative coordinates", - x: -1, y: -1, + name: "click at negative coordinates", + x: -1, y: -1, panelBounds: map[Panel]Rect{ PanelCurrentWork: {X: 0, Y: 0, W: 100, H: 10}, }, @@ -1231,8 +1231,8 @@ func TestMouseClickEdgeCases(t *testing.T) { description: "negative coordinates out of bounds", }, { - name: "click at very large coordinates", - x: 9999, y: 9999, + name: "click at very large coordinates", + x: 9999, y: 9999, panelBounds: map[Panel]Rect{ PanelCurrentWork: {X: 0, Y: 0, W: 100, H: 10}, }, diff --git a/pkg/monitor/keymap/registry.go b/pkg/monitor/keymap/registry.go index 5845ba57..3b1d87ea 100644 --- a/pkg/monitor/keymap/registry.go +++ b/pkg/monitor/keymap/registry.go @@ -29,13 +29,13 @@ const ( ContextHelp Context = "help" // When help modal is open ContextBoardPicker Context = "board-picker" // When board picker is open ContextBoard Context = "board" // When board mode is active - ContextGettingStarted Context = "getting-started" // When getting started modal is open - ContextTDQHelp Context = "tdq-help" // When TDQ help modal is open - ContextBoardEditor Context = "board-editor" // When board edit/create modal is open - ContextCloseConfirm Context = "close-confirm" // When close confirmation modal is open (has text input) - ContextSyncPrompt Context = "td-sync-prompt" // When sync prompt modal is open - ContextKanban Context = "kanban" // When kanban view modal is open - ContextNotes Context = "notes" // When notes modal is open + ContextGettingStarted Context = "getting-started" // When getting started modal is open + ContextTDQHelp Context = "tdq-help" // When TDQ help modal is open + ContextBoardEditor Context = "board-editor" // When board edit/create modal is open + ContextCloseConfirm Context = "close-confirm" // When close confirmation modal is open (has text input) + ContextSyncPrompt Context = "td-sync-prompt" // When sync prompt modal is open + ContextKanban Context = "kanban" // When kanban view modal is open + ContextNotes Context = "notes" // When notes modal is open ) // Command represents a named command that can be triggered by key bindings @@ -52,32 +52,32 @@ const ( CmdNextPanel Command = "next-panel" CmdPrevPanel Command = "prev-panel" CmdCursorDown Command = "cursor-down" - CmdCursorUp Command = "cursor-up" - CmdCursorTop Command = "cursor-top" - CmdCursorBottom Command = "cursor-bottom" - CmdHalfPageDown Command = "half-page-down" - CmdHalfPageUp Command = "half-page-up" - CmdFullPageDown Command = "full-page-down" - CmdFullPageUp Command = "full-page-up" - CmdScrollDown Command = "scroll-down" - CmdScrollUp Command = "scroll-up" - CmdSelect Command = "select" - CmdBack Command = "back" - CmdClose Command = "close" - CmdNavigatePrev Command = "navigate-prev" - CmdNavigateNext Command = "navigate-next" + CmdCursorUp Command = "cursor-up" + CmdCursorTop Command = "cursor-top" + CmdCursorBottom Command = "cursor-bottom" + CmdHalfPageDown Command = "half-page-down" + CmdHalfPageUp Command = "half-page-up" + CmdFullPageDown Command = "full-page-down" + CmdFullPageUp Command = "full-page-up" + CmdScrollDown Command = "scroll-down" + CmdScrollUp Command = "scroll-up" + CmdSelect Command = "select" + CmdBack Command = "back" + CmdClose Command = "close" + CmdNavigatePrev Command = "navigate-prev" + CmdNavigateNext Command = "navigate-next" // Action commands - CmdOpenDetails Command = "open-details" - CmdOpenStats Command = "open-stats" - CmdSearch Command = "search" - CmdToggleClosed Command = "toggle-closed" - CmdMarkForReview Command = "mark-for-review" - CmdApprove Command = "approve" - CmdDelete Command = "delete" - CmdConfirm Command = "confirm" - CmdCancel Command = "cancel" - CmdCycleSortMode Command = "cycle-sort-mode" + CmdOpenDetails Command = "open-details" + CmdOpenStats Command = "open-stats" + CmdSearch Command = "search" + CmdToggleClosed Command = "toggle-closed" + CmdMarkForReview Command = "mark-for-review" + CmdApprove Command = "approve" + CmdDelete Command = "delete" + CmdConfirm Command = "confirm" + CmdCancel Command = "cancel" + CmdCycleSortMode Command = "cycle-sort-mode" // Search-specific commands CmdSearchConfirm Command = "search-confirm" @@ -143,19 +143,19 @@ const ( CmdSendToWorktree Command = "send-to-worktree" // Board editor commands - CmdEditBoard Command = "edit-board" - CmdNewBoard Command = "new-board" - CmdBoardEditorSave Command = "board-editor-save" - CmdBoardEditorCancel Command = "board-editor-cancel" - CmdBoardEditorDelete Command = "board-editor-delete" + CmdEditBoard Command = "edit-board" + CmdNewBoard Command = "new-board" + CmdBoardEditorSave Command = "board-editor-save" + CmdBoardEditorCancel Command = "board-editor-cancel" + CmdBoardEditorDelete Command = "board-editor-delete" // Getting started commands CmdOpenGettingStarted Command = "open-getting-started" CmdInstallInstructions Command = "install-instructions" // Kanban view commands - CmdOpenKanban Command = "open-kanban" - CmdCloseKanban Command = "close-kanban" + CmdOpenKanban Command = "open-kanban" + CmdCloseKanban Command = "close-kanban" CmdToggleKanbanFullscreen Command = "toggle-kanban-fullscreen" ) diff --git a/pkg/monitor/model.go b/pkg/monitor/model.go index 57d96ec9..5ed9437a 100644 --- a/pkg/monitor/model.go +++ b/pkg/monitor/model.go @@ -116,7 +116,7 @@ type Model struct { // Activity detail modal state ActivityDetailOpen bool - ActivityDetailItem *ActivityItem // The selected activity item + ActivityDetailItem *ActivityItem // The selected activity item ActivityDetailScroll int ActivityDetailModal *modal.Modal // Declarative modal instance ActivityDetailMouseHandler *mouse.Handler // Mouse handler for activity detail modal @@ -128,8 +128,8 @@ type Model struct { NotesMouseHandler *mouse.Handler // Mouse handler for notes modal // Form modal state - FormOpen bool - FormState *FormState + FormOpen bool + FormState *FormState FormScrollOffset int // Scroll offset for form modal when content overflows // Getting Started modal state diff --git a/pkg/monitor/model_test.go b/pkg/monitor/model_test.go index 7d2793dc..f4366adc 100644 --- a/pkg/monitor/model_test.go +++ b/pkg/monitor/model_test.go @@ -4312,10 +4312,10 @@ func TestSwimlaneLinesFromOffset(t *testing.T) { }{ // Note: at offset 0, currentCategory starts as zero-value, so the first // category always triggers a header (matching rendering behavior). - {"all from start", 0, 5, 10}, // header(ready)+2items + sep+header(blocked)+2items + sep+header(closed)+1item = 1+2+2+2+2+1=10 - {"single category", 0, 2, 3}, // header(ready) + 2 items = 3 - {"across boundary", 1, 4, 5}, // item2 + sep+header(blocked) + item3 + item4 = 5 - {"from second cat", 2, 5, 6}, // header(blocked)+item3+item4 + sep+header(closed)+item5 = 1+2+2+1=6 + {"all from start", 0, 5, 10}, // header(ready)+2items + sep+header(blocked)+2items + sep+header(closed)+1item = 1+2+2+2+2+1=10 + {"single category", 0, 2, 3}, // header(ready) + 2 items = 3 + {"across boundary", 1, 4, 5}, // item2 + sep+header(blocked) + item3 + item4 = 5 + {"from second cat", 2, 5, 6}, // header(blocked)+item3+item4 + sep+header(closed)+item5 = 1+2+2+1=6 {"empty range", 3, 3, 0}, {"single item last cat", 4, 5, 2}, // header(closed) + item5 = 2 {"single item same cat", 1, 2, 1}, // just item2, same category as item1 before it diff --git a/pkg/monitor/notes_lint_refs.go b/pkg/monitor/notes_lint_refs.go new file mode 100644 index 00000000..cd9178e5 --- /dev/null +++ b/pkg/monitor/notes_lint_refs.go @@ -0,0 +1,28 @@ +package monitor + +// Keep the dormant notes modal scaffolding reachable until the feature is wired +// back into the active monitor flows. +var ( + _ = 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 + _ = formatNoteListItem + _ = formatNoteMeta + _ = formatNoteAge + _ = Model.renderNotesModal + _ = Model.wrapSimpleModal + _ = modalBorderStyle +) diff --git a/pkg/monitor/notes_modal.go b/pkg/monitor/notes_modal.go index 5e845acb..2bf3ef51 100644 --- a/pkg/monitor/notes_modal.go +++ b/pkg/monitor/notes_modal.go @@ -19,20 +19,20 @@ import ( // 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 diff --git a/pkg/monitor/submit_to_review_test.go b/pkg/monitor/submit_to_review_test.go index 6260e4a3..74b7c353 100644 --- a/pkg/monitor/submit_to_review_test.go +++ b/pkg/monitor/submit_to_review_test.go @@ -107,46 +107,46 @@ func TestMarkForReviewCommandExecution(t *testing.T) { // TestSubmitToReviewStateTransition is a table-driven test for state transitions func TestSubmitToReviewStateTransition(t *testing.T) { tests := []struct { - name string - initialStatus models.Status - expectedStatus models.Status + name string + initialStatus models.Status + expectedStatus models.Status shouldTransition bool - description string + description string }{ { - name: "open issue transitions to in_review", - initialStatus: models.StatusOpen, - expectedStatus: models.StatusInReview, + name: "open issue transitions to in_review", + initialStatus: models.StatusOpen, + expectedStatus: models.StatusInReview, shouldTransition: true, - description: "Ready issues can be submitted for review", + description: "Ready issues can be submitted for review", }, { - name: "in_progress issue transitions to in_review", - initialStatus: models.StatusInProgress, - expectedStatus: models.StatusInReview, + name: "in_progress issue transitions to in_review", + initialStatus: models.StatusInProgress, + expectedStatus: models.StatusInReview, shouldTransition: true, - description: "In-progress issues can be submitted for review", + description: "In-progress issues can be submitted for review", }, { - name: "in_review issue stays in_review", - initialStatus: models.StatusInReview, - expectedStatus: models.StatusInReview, + name: "in_review issue stays in_review", + initialStatus: models.StatusInReview, + expectedStatus: models.StatusInReview, shouldTransition: false, - description: "Already reviewed issues cannot be re-reviewed", + description: "Already reviewed issues cannot be re-reviewed", }, { - name: "closed issue stays closed", - initialStatus: models.StatusClosed, - expectedStatus: models.StatusClosed, + name: "closed issue stays closed", + initialStatus: models.StatusClosed, + expectedStatus: models.StatusClosed, shouldTransition: false, - description: "Closed issues cannot be submitted for review", + description: "Closed issues cannot be submitted for review", }, { - name: "blocked issue stays blocked", - initialStatus: models.StatusBlocked, - expectedStatus: models.StatusBlocked, + name: "blocked issue stays blocked", + initialStatus: models.StatusBlocked, + expectedStatus: models.StatusBlocked, shouldTransition: false, - description: "Blocked issues cannot be submitted for review", + description: "Blocked issues cannot be submitted for review", }, } @@ -161,7 +161,7 @@ func TestSubmitToReviewStateTransition(t *testing.T) { // Simulate the validation logic from markForReview allowReview := (issue.Status == models.StatusInProgress || - issue.Status == models.StatusOpen) + issue.Status == models.StatusOpen) if tt.shouldTransition { if !allowReview { @@ -190,32 +190,32 @@ func TestSubmitToReviewStateTransition(t *testing.T) { // TestSubmitToReviewModalHandling verifies modal closes after submission func TestSubmitToReviewModalHandling(t *testing.T) { tests := []struct { - name string - modalOpen bool + name string + modalOpen bool expectedModalOpen bool - description string + description string }{ { - name: "modal should close after review submission", - modalOpen: true, + name: "modal should close after review submission", + modalOpen: true, expectedModalOpen: false, - description: "Modal closes when issue transitions to review", + description: "Modal closes when issue transitions to review", }, { - name: "main panel submission keeps panel active", - modalOpen: false, + name: "main panel submission keeps panel active", + modalOpen: false, expectedModalOpen: false, - description: "Main panel remains active after submission", + description: "Main panel remains active after submission", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := Model{ - Keymap: newTestKeymap(), - ModalStack: []ModalEntry{}, - ActivePanel: PanelTaskList, - SessionID: "test-session", + Keymap: newTestKeymap(), + ModalStack: []ModalEntry{}, + ActivePanel: PanelTaskList, + SessionID: "test-session", } // Set up modal if test expects it @@ -342,12 +342,12 @@ func TestMarkForReviewFromCurrentWorkPanel(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := Model{ - Keymap: newTestKeymap(), - ActivePanel: PanelCurrentWork, - Cursor: map[Panel]int{PanelCurrentWork: tt.cursorPos}, - SelectedID: map[Panel]string{}, + Keymap: newTestKeymap(), + ActivePanel: PanelCurrentWork, + Cursor: map[Panel]int{PanelCurrentWork: tt.cursorPos}, + SelectedID: map[Panel]string{}, FocusedIssue: tt.focusedIssue, - InProgress: tt.inProgress, + InProgress: tt.inProgress, } // Build current work rows @@ -536,19 +536,19 @@ func TestHandleKeyShiftRInModalContext(t *testing.T) { // TestStatusMessageAfterSubmit verifies user feedback func TestStatusMessageAfterSubmit(t *testing.T) { tests := []struct { - name string - shouldShowMsg bool - description string + name string + shouldShowMsg bool + description string }{ { name: "transition to in_review", shouldShowMsg: true, - description: "User sees feedback when issue submitted for review", + description: "User sees feedback when issue submitted for review", }, { name: "already in review (no action)", shouldShowMsg: false, - description: "No message when action has no effect", + description: "No message when action has no effect", }, } @@ -617,16 +617,16 @@ func TestReviewActionLogging(t *testing.T) { // TestContextDetectionWithModals verifies correct context selection func TestContextDetectionWithModals(t *testing.T) { tests := []struct { - name string - model Model + name string + model Model expectedContext keymap.Context }{ { name: "main context without modals", model: Model{ - Keymap: newTestKeymap(), - ModalStack: []ModalEntry{}, - SearchMode: false, + Keymap: newTestKeymap(), + ModalStack: []ModalEntry{}, + SearchMode: false, }, expectedContext: keymap.ContextMain, }, diff --git a/pkg/monitor/sync_prompt.go b/pkg/monitor/sync_prompt.go index 89f8d259..97437d0f 100644 --- a/pkg/monitor/sync_prompt.go +++ b/pkg/monitor/sync_prompt.go @@ -7,8 +7,8 @@ import ( "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" - "github.com/marcus/td/internal/syncconfig" "github.com/marcus/td/internal/syncclient" + "github.com/marcus/td/internal/syncconfig" "github.com/marcus/td/pkg/monitor/modal" "github.com/marcus/td/pkg/monitor/mouse" ) diff --git a/pkg/monitor/sync_prompt_test.go b/pkg/monitor/sync_prompt_test.go index 6baebc5a..ad1e5dc7 100644 --- a/pkg/monitor/sync_prompt_test.go +++ b/pkg/monitor/sync_prompt_test.go @@ -342,4 +342,3 @@ func TestIsFirstRunInit_Guards_SyncPrompt(t *testing.T) { } }) } - diff --git a/pkg/monitor/types.go b/pkg/monitor/types.go index 147460b4..24303746 100644 --- a/pkg/monitor/types.go +++ b/pkg/monitor/types.go @@ -428,9 +428,9 @@ type boardEditorDebounceMsg struct { // BoardEditorSaveResultMsg carries the result of saving a board type BoardEditorSaveResultMsg struct { - Board *models.Board - IsNew bool // true if newly created, false if updated - Error error + Board *models.Board + IsNew bool // true if newly created, false if updated + Error error } // BoardEditorDeleteResultMsg carries the result of deleting a board @@ -441,10 +441,10 @@ type BoardEditorDeleteResultMsg struct { // BoardEditorQueryPreviewMsg carries live query preview results type BoardEditorQueryPreviewMsg struct { - Query string // Query that was executed (for staleness check) - Count int - Titles []string // First 5 issue titles - Error error + Query string // Query that was executed (for staleness check) + Count int + Titles []string // First 5 issue titles + Error error } // boardEditorPreviewData holds live query preview state. @@ -470,10 +470,10 @@ type BoardMode struct { ViewMode BoardViewMode // Current view mode // Swimlanes view state (separate cursor/scroll from backlog) - SwimlaneData TaskListData // Categorized data for swimlanes view - SwimlaneRows []TaskListRow // Flattened rows for swimlanes view - SwimlaneCursor int // Cursor position in swimlanes view - SwimlaneScroll int // Scroll offset in swimlanes view + SwimlaneData TaskListData // Categorized data for swimlanes view + SwimlaneRows []TaskListRow // Flattened rows for swimlanes view + SwimlaneCursor int // Cursor position in swimlanes view + SwimlaneScroll int // Scroll offset in swimlanes view // Selection restoration after move operations PendingSelectionID string // Issue ID to select after refresh (cleared after use) @@ -494,13 +494,13 @@ func DefaultBoardStatusFilter() map[models.Status]bool { type StatusFilterPreset int const ( - StatusPresetDefault StatusFilterPreset = iota // open/in_progress/blocked/in_review - StatusPresetAll // all statuses - StatusPresetOpen // only open - StatusPresetInProgress // only in_progress - StatusPresetBlocked // only blocked - StatusPresetInReview // only in_review - StatusPresetClosed // only closed + StatusPresetDefault StatusFilterPreset = iota // open/in_progress/blocked/in_review + StatusPresetAll // all statuses + StatusPresetOpen // only open + StatusPresetInProgress // only in_progress + StatusPresetBlocked // only blocked + StatusPresetInReview // only in_review + StatusPresetClosed // only closed ) // StatusFilterPresetName returns the display name for a preset diff --git a/test/e2e/engine.go b/test/e2e/engine.go index 455d0cf1..099b1c20 100644 --- a/test/e2e/engine.go +++ b/test/e2e/engine.go @@ -32,8 +32,8 @@ type IssueState struct { // ActionStats tracks per-action-type outcomes. type ActionStats struct { - OK int - ExpFail int + OK int + ExpFail int UnexpFail int } @@ -62,10 +62,10 @@ type ChaosEngine struct { Issues map[string]*IssueState // id -> state IssueOrder []string // ordered list of all created issue IDs Boards []string - DepPairs map[string]bool // "from_to" -> true - ParentChild map[string]string // childID -> parentID - IssueFiles map[string]string // "issueID~filePath" -> role - ActiveWS map[string]string // actor -> ws name + DepPairs map[string]bool // "from_to" -> true + ParentChild map[string]string // childID -> parentID + IssueFiles map[string]string // "issueID~filePath" -> role + ActiveWS map[string]string // actor -> ws name WSTagged map[string]map[string]bool // actor -> set of tagged issue IDs Stats ChaosStats diff --git a/test/e2e/history.go b/test/e2e/history.go index a98e4072..bf400a7e 100644 --- a/test/e2e/history.go +++ b/test/e2e/history.go @@ -26,14 +26,14 @@ type OperationRecord struct { // HistorySummary holds aggregate stats over recorded operations. type HistorySummary struct { - TotalOps int `json:"total_ops"` - ByResult map[string]int `json:"by_result"` - ByAction map[string]int `json:"by_action"` - ByActor map[string]int `json:"by_actor"` - AvgDuration time.Duration `json:"avg_duration_ns"` - MaxDuration time.Duration `json:"max_duration_ns"` - TotalDuration time.Duration `json:"total_duration_ns"` - UniqueIssues int `json:"unique_issues"` + TotalOps int `json:"total_ops"` + ByResult map[string]int `json:"by_result"` + ByAction map[string]int `json:"by_action"` + ByActor map[string]int `json:"by_actor"` + AvgDuration time.Duration `json:"avg_duration_ns"` + MaxDuration time.Duration `json:"max_duration_ns"` + TotalDuration time.Duration `json:"total_duration_ns"` + UniqueIssues int `json:"unique_issues"` } // OperationHistory records all operations performed during a chaos run. diff --git a/test/e2e/random.go b/test/e2e/random.go index bbfd6671..5467602e 100644 --- a/test/e2e/random.go +++ b/test/e2e/random.go @@ -8,12 +8,12 @@ import ( // Edge-case strings for adversarial testing. var edgeStrings = []string{ - "", // empty - "x", // single char - strings.Repeat("A", 1200), // very long + "", // empty + "x", // single char + strings.Repeat("A", 1200), // very long "\xf0\x9f\x94\xa5\xf0\x9f\x90\x9b\xe2\x9c\x85\xf0\x9f\x9a\x80\xf0\x9f\x92\x80\xf0\x9f\x8e\x89", // emoji - "\u6d4b\u8bd5\u4e2d\u6587\u6570\u636e\u5904\u7406", // CJK - "\u0645\u0631\u062d\u0628\u0627 \u0628\u0627\u0644\u0639\u0627\u0644\u0645", // RTL Arabic + "\u6d4b\u8bd5\u4e2d\u6587\u6570\u636e\u5904\u7406", // CJK + "\u0645\u0631\u062d\u0628\u0627 \u0628\u0627\u0644\u0639\u0627\u0644\u0645", // RTL Arabic "line one\nline two\nline three", "it's a test with 'single quotes'", `she said "hello world"`, diff --git a/test/e2e/report.go b/test/e2e/report.go index 9d567014..428693bd 100644 --- a/test/e2e/report.go +++ b/test/e2e/report.go @@ -4,24 +4,24 @@ import "time" // ChaosReport is the JSON-serializable report for CI integration. type ChaosReport struct { - Seed int64 `json:"seed"` - Actions int `json:"actions"` - Duration int64 `json:"duration_ms"` - Actors int `json:"actors"` - Results ReportResults `json:"results"` - PerAction map[string]ReportActionStats `json:"per_action"` - Verifications []ReportVerification `json:"verifications"` - SyncStats ReportSyncStats `json:"sync_stats"` - Pass bool `json:"pass"` + Seed int64 `json:"seed"` + Actions int `json:"actions"` + Duration int64 `json:"duration_ms"` + Actors int `json:"actors"` + Results ReportResults `json:"results"` + PerAction map[string]ReportActionStats `json:"per_action"` + Verifications []ReportVerification `json:"verifications"` + SyncStats ReportSyncStats `json:"sync_stats"` + Pass bool `json:"pass"` } // ReportResults aggregates action outcomes. type ReportResults struct { - Total int `json:"total"` - OK int `json:"ok"` - ExpFail int `json:"expected_fail"` - UnexpFail int `json:"unexpected_fail"` - Skipped int `json:"skipped"` + Total int `json:"total"` + OK int `json:"ok"` + ExpFail int `json:"expected_fail"` + UnexpFail int `json:"unexpected_fail"` + Skipped int `json:"skipped"` } // ReportActionStats tracks per-action-type outcomes. diff --git a/test/e2e/restart.go b/test/e2e/restart.go index 9dd2a270..30bbeb7d 100644 --- a/test/e2e/restart.go +++ b/test/e2e/restart.go @@ -190,4 +190,3 @@ func countIssues(h *Harness, actor string) int { } return count } - diff --git a/test/e2e/verify.go b/test/e2e/verify.go index e53f3c41..9b28566f 100644 --- a/test/e2e/verify.go +++ b/test/e2e/verify.go @@ -327,8 +327,8 @@ func (v *Verifier) VerifyCausalOrdering(actor string) []VerifyResult { } // Track first-seen action per entity - created := make(map[string]int) // entity_id -> server_seq of create - started := make(map[string]int) // issue_id -> server_seq of start + created := make(map[string]int) // entity_id -> server_seq of create + started := make(map[string]int) // issue_id -> server_seq of start violations := 0 var details []string @@ -616,5 +616,3 @@ func sqlInClause(ids []string) string { } return strings.Join(quoted, ",") } - - diff --git a/test/syncharness/field_merge_test.go b/test/syncharness/field_merge_test.go index f19f9a72..605e7b28 100644 --- a/test/syncharness/field_merge_test.go +++ b/test/syncharness/field_merge_test.go @@ -193,7 +193,7 @@ func TestUpdateWithNoPreviousDataFallback(t *testing.T) { EntityType: "issues", EntityID: "td-FM3", Payload: []byte(payload), - ClientTimestamp: time.Now(), + ClientTimestamp: time.Now(), } // Insert directly into server @@ -252,7 +252,7 @@ func TestUpdateNonExistentRowFallback(t *testing.T) { EntityType: "issues", EntityID: "td-FM4", Payload: []byte(payload), - ClientTimestamp: time.Now(), + ClientTimestamp: time.Now(), } // Insert directly into server @@ -326,7 +326,7 @@ func TestEmptyDiffNoOp(t *testing.T) { EntityType: "issues", EntityID: "td-FM5", Payload: []byte(payload), - ClientTimestamp: time.Now(), + ClientTimestamp: time.Now(), } // Insert directly into server diff --git a/test/syncharness/harness.go b/test/syncharness/harness.go index 65e98f62..a33ca12f 100644 --- a/test/syncharness/harness.go +++ b/test/syncharness/harness.go @@ -590,10 +590,10 @@ var softDeleteTables = map[string]bool{ // that mapActionType() converts to hard "delete" (not "soft_delete"). // Without this, "delete" action on these tables would become "soft_delete" and fail. var hardDeleteActionTypes = map[string]string{ - "issue_dependencies": "remove_dependency", - "issue_files": "unlink_file", - "work_session_issues": "work_session_untag", - "boards": "board_delete", + "issue_dependencies": "remove_dependency", + "issue_files": "unlink_file", + "work_session_issues": "work_session_untag", + "boards": "board_delete", } // dumpTable returns a deterministic string representation of all rows in a table. diff --git a/test/syncharness/harness_test.go b/test/syncharness/harness_test.go index ab74b3cf..ab98239c 100644 --- a/test/syncharness/harness_test.go +++ b/test/syncharness/harness_test.go @@ -802,7 +802,7 @@ func TestSchemaVersionMismatch(t *testing.T) { EntityType: "issues", EntityID: "td-SV1", Payload: []byte(payload), - ClientTimestamp: time.Now(), + ClientTimestamp: time.Now(), } // Insert directly into server @@ -861,7 +861,7 @@ func TestPartialBatchFailure(t *testing.T) { EntityType: "issues", EntityID: fmt.Sprintf("td-PB%d", i), Payload: []byte(payload), - ClientTimestamp: now, + ClientTimestamp: now, }) } @@ -874,7 +874,7 @@ func TestPartialBatchFailure(t *testing.T) { EntityType: "nonexistent_table", EntityID: "td-BAD", Payload: []byte(`{"schema_version":1,"new_data":{"name":"bad"},"previous_data":{}}`), - ClientTimestamp: now, + ClientTimestamp: now, }) for i := 5; i <= 7; i++ { @@ -887,7 +887,7 @@ func TestPartialBatchFailure(t *testing.T) { EntityType: "issues", EntityID: fmt.Sprintf("td-PB%d", i), Payload: []byte(payload), - ClientTimestamp: now, + ClientTimestamp: now, }) } diff --git a/test/syncharness/server_migration_test.go b/test/syncharness/server_migration_test.go index 98c430f1..630e6cf1 100644 --- a/test/syncharness/server_migration_test.go +++ b/test/syncharness/server_migration_test.go @@ -7,9 +7,9 @@ import ( // TestServerMigration verifies that a client can re-sync all events after the server // loses track of previously synced events (e.g., migration to a new server). // The scenario: -// 1. Client creates an issue, pushes to server - synced_at is set -// 2. Server "dies" - simulate by clearing synced_at on client's action_log -// 3. Client pushes again - events should sync successfully to the new server +// 1. Client creates an issue, pushes to server - synced_at is set +// 2. Server "dies" - simulate by clearing synced_at on client's action_log +// 3. Client pushes again - events should sync successfully to the new server func TestServerMigration(t *testing.T) { const projID = "proj-migration" h := NewHarness(t, 1, projID)