From 0d50ee1f13e8daa05b6f637973ece473843e7c9a Mon Sep 17 00:00:00 2001 From: tester Date: Tue, 21 Apr 2026 03:18:34 -0700 Subject: [PATCH] style: run gofmt on lint-failing files Nightshift-Task: lint-fix Nightshift-Ref: https://github.com/marcus/nightshift --- cmd/block_test.go | 4 +- cmd/errors_test.go | 6 +- cmd/sync_tail.go | 4 +- cmd/sync_tail_test.go | 2 +- cmd/system.go | 10 +- internal/api/action_log_promotion.go | 1 - internal/api/action_log_promotion_test.go | 1 - internal/api/admin_projects_test.go | 10 +- internal/api/metrics_lag_test.go | 1 - internal/db/activity_test.go | 6 +- internal/db/fk_audit.go | 4 +- internal/db/fk_audit_test.go | 18 +- internal/db/ids.go | 2 +- internal/db/issue_relations_test.go | 1 - internal/db/migration_fk_enforcement.go | 14 +- internal/db/sync_state_test.go | 6 +- internal/features/features_test.go | 31 ++ internal/query/ast.go | 12 +- internal/query/execute.go | 2 +- internal/query/execute_test.go | 6 +- internal/query/lexer_test.go | 4 +- internal/serve/session.go | 10 +- internal/serverdb/apikeys.go | 6 +- internal/serverdb/auth_events.go | 24 +- internal/session/agent_fingerprint_test.go | 62 ++-- internal/session/session.go | 8 +- internal/sync/applyrepro_test.go | 192 +++++++------ internal/sync/client.go | 10 +- internal/sync/events.go | 8 +- 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_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 +- 55 files changed, 610 insertions(+), 568 deletions(-) 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/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/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..634f5cec 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"}, }, } diff --git a/cmd/system.go b/cmd/system.go index 786010d3..db550678 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: diff --git a/internal/api/action_log_promotion.go b/internal/api/action_log_promotion.go index deabb1ae..5f15ce88 100644 --- a/internal/api/action_log_promotion.go +++ b/internal/api/action_log_promotion.go @@ -238,4 +238,3 @@ func shouldPromote(method string) bool { } return true } - diff --git a/internal/api/action_log_promotion_test.go b/internal/api/action_log_promotion_test.go index c7585705..decb4b91 100644 --- a/internal/api/action_log_promotion_test.go +++ b/internal/api/action_log_promotion_test.go @@ -454,4 +454,3 @@ func TestPromote_RecoveryAfterError(t *testing.T) { t.Errorf("events count = %d, want 1", len(events)) } } - diff --git a/internal/api/admin_projects_test.go b/internal/api/admin_projects_test.go index 7318c0af..2d37b6a3 100644 --- a/internal/api/admin_projects_test.go +++ b/internal/api/admin_projects_test.go @@ -319,11 +319,11 @@ func TestAdminSyncCursors_BackfillFromEvents(t *testing.T) { events := make([]EventInput, count) for i := 0; i < count; i++ { events[i] = EventInput{ - ClientActionID: startID + int64(i), - ActionType: "create", - EntityType: "issues", - EntityID: fmt.Sprintf("i_%s_%d", dev, i), - Payload: json.RawMessage(`{"title":"x"}`), + ClientActionID: startID + int64(i), + ActionType: "create", + EntityType: "issues", + EntityID: fmt.Sprintf("i_%s_%d", dev, i), + Payload: json.RawMessage(`{"title":"x"}`), ClientTimestamp: "2025-01-01T00:00:00Z", } } diff --git a/internal/api/metrics_lag_test.go b/internal/api/metrics_lag_test.go index 9b6a22a7..fa2db866 100644 --- a/internal/api/metrics_lag_test.go +++ b/internal/api/metrics_lag_test.go @@ -423,4 +423,3 @@ func TestPoolSnapshot_NoDeadlock(t *testing.T) { close(stop) wg.Wait() } - 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/fk_audit.go b/internal/db/fk_audit.go index c0689b56..55873908 100644 --- a/internal/db/fk_audit.go +++ b/internal/db/fk_audit.go @@ -8,9 +8,9 @@ import ( // OrphanCount reports the number of rows in a child table whose foreign-key // column points at a non-existent row in the parent table. // -// Count = number of child rows where child.col NOT NULL / NOT '' AND no +// Count = number of child rows where child.col NOT NULL / NOT "" AND no // matching parent row exists. We treat empty-string and NULL as "no link" -// to match td's mixed-sentinel convention (many FK columns default to ''). +// to match td's mixed-sentinel convention (many FK columns default to ""). type OrphanCount struct { Relation string // e.g. "handoffs.issue_id -> issues.id" ChildTable string diff --git a/internal/db/fk_audit_test.go b/internal/db/fk_audit_test.go index 8c22e072..9de4d2ca 100644 --- a/internal/db/fk_audit_test.go +++ b/internal/db/fk_audit_test.go @@ -95,15 +95,15 @@ func TestAuditForeignKeys_DetectsOrphans(t *testing.T) { } expect := map[string]int{ - "issues.parent_id -> issues.id": 1, - "handoffs.issue_id -> issues.id": 1, - "git_snapshots.issue_id -> issues.id": 1, - "issue_files.issue_id -> issues.id": 1, - "issue_dependencies.issue_id -> issues.id": 1, - "issue_dependencies.depends_on_id -> issues.id": 1, + "issues.parent_id -> issues.id": 1, + "handoffs.issue_id -> issues.id": 1, + "git_snapshots.issue_id -> issues.id": 1, + "issue_files.issue_id -> issues.id": 1, + "issue_dependencies.issue_id -> issues.id": 1, + "issue_dependencies.depends_on_id -> issues.id": 1, "work_session_issues.work_session_id -> work_sessions.id": 1, - "work_session_issues.issue_id -> issues.id": 1, - "comments.issue_id -> issues.id": 1, + "work_session_issues.issue_id -> issues.id": 1, + "comments.issue_id -> issues.id": 1, } for rel, want := range expect { @@ -114,7 +114,7 @@ func TestAuditForeignKeys_DetectsOrphans(t *testing.T) { } // TestAuditForeignKeys_IgnoresEmptyFKValues ensures default-empty FK columns -// (e.g. issues.parent_id='') are not counted as orphans. +// (e.g. issues.parent_id="") are not counted as orphans. func TestAuditForeignKeys_IgnoresEmptyFKValues(t *testing.T) { dir := t.TempDir() database, err := Initialize(dir) 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 32cfbfc6..0347e016 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/migration_fk_enforcement.go b/internal/db/migration_fk_enforcement.go index 3cb141f3..527a89c3 100644 --- a/internal/db/migration_fk_enforcement.go +++ b/internal/db/migration_fk_enforcement.go @@ -12,10 +12,10 @@ import ( // // 1. Cleans up the FK orphans identified by the Wave 1 audit (td-b8dd0d). // Policies (justified per relation): -// - issues.parent_id: nullify to '' (empty-string sentinel) — preserve -// the issue itself; parent link was dangling. +// - issues.parent_id: nullify to "" (empty-string sentinel) — preserve +// the issue itself; parent link was dangling. // - handoffs/git_snapshots/issue_dependencies/issue_session_history: -// DELETE — a row referring to a missing issue has no meaningful value. +// DELETE — a row referring to a missing issue has no meaningful value. // 2. Rewrites child tables to add ON DELETE CASCADE where td's // internal/sync/events.go already performs a manual cascade delete. This // makes the DB enforce what the app code does. internal/sync/events.go @@ -23,13 +23,13 @@ import ( // after the manual path removes rows). td-0001eb will remove the // manual emulation once this is in. // 3. issues.parent_id FK: the constraint is DROPPED at the schema level. -// Rationale: td's codebase uses '' (empty string) as the "no parent" +// Rationale: td's codebase uses "" (empty string) as the "no parent" // sentinel throughout (INSERTs, UPDATEs, sync event payloads). SQLite -// FK semantics treat '' as a real value, not as NULL, so a schema-level +// FK semantics treat "" as a real value, not as NULL, so a schema-level // FK on parent_id would reject every top-level issue insert (since no -// issue has id = ''). Migrating every writer to use NULL is out of +// issue has id = ""). Migrating every writer to use NULL is out of // scope for td-4846e6; keeping parent_id unconstrained matches the -// long-standing behaviour, preserves the audit (which treats '' as not +// long-standing behaviour, preserves the audit (which treats "" as not // a link), and leaves deleting a parent as non-cascading (children // become orphans with stale parent_ids until the app or a follow-up // cleanup rewrites them). The fk_audit keeps flagging any true orphans. 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/features/features_test.go b/internal/features/features_test.go index 4b18b24a..6cc6c83f 100644 --- a/internal/features/features_test.go +++ b/internal/features/features_test.go @@ -6,7 +6,28 @@ import ( "github.com/marcus/td/internal/config" ) +func resetFeatureEnv(t *testing.T) { + t.Helper() + + for _, key := range []string{ + "TD_DISABLE_EXPERIMENTAL", + "TD_ENABLE_FEATURE", + "TD_ENABLE_FEATURES", + "TD_DISABLE_FEATURE", + "TD_DISABLE_FEATURES", + "TD_FEATURE_BALANCED_REVIEW_POLICY", + "TD_FEATURE_SYNC_AUTOSYNC", + "TD_FEATURE_SYNC_CLI", + "TD_FEATURE_SYNC_MONITOR_PROMPT", + "TD_FEATURE_SYNC_NOTES", + } { + t.Setenv(key, "") + } +} + func TestKnownFeatureDefaults(t *testing.T) { + resetFeatureEnv(t) + for _, feature := range ListAll() { if IsEnabledForProcess(feature.Name) != feature.Default { t.Fatalf("default mismatch for %s", feature.Name) @@ -15,6 +36,8 @@ func TestKnownFeatureDefaults(t *testing.T) { } func TestIsEnabledForProcess_EnvVarOverride(t *testing.T) { + resetFeatureEnv(t) + t.Setenv("TD_FEATURE_SYNC_CLI", "true") if !IsEnabledForProcess(SyncCLI.Name) { t.Fatal("TD_FEATURE_SYNC_CLI=true should enable sync_cli") @@ -27,6 +50,8 @@ func TestIsEnabledForProcess_EnvVarOverride(t *testing.T) { } func TestIsEnabledForProcess_EnableDisableLists(t *testing.T) { + resetFeatureEnv(t) + t.Setenv("TD_ENABLE_FEATURE", "sync_cli,sync_monitor_prompt") if !IsEnabledForProcess(SyncCLI.Name) { t.Fatal("TD_ENABLE_FEATURE should enable sync_cli") @@ -42,6 +67,8 @@ func TestIsEnabledForProcess_EnableDisableLists(t *testing.T) { } func TestIsEnabled_ProjectConfigAndEnvPrecedence(t *testing.T) { + resetFeatureEnv(t) + dir := t.TempDir() if err := config.SetFeatureFlag(dir, SyncCLI.Name, true); err != nil { @@ -60,6 +87,8 @@ func TestIsEnabled_ProjectConfigAndEnvPrecedence(t *testing.T) { } func TestDisableExperimentalKillSwitch(t *testing.T) { + resetFeatureEnv(t) + t.Setenv("TD_ENABLE_FEATURE", "sync_cli") if !IsEnabledForProcess(SyncCLI.Name) { t.Fatal("expected sync_cli enabled before kill-switch") @@ -75,6 +104,8 @@ func TestDisableExperimentalKillSwitch(t *testing.T) { } func TestSyncGateMapReferencesKnownFeatures(t *testing.T) { + resetFeatureEnv(t) + if len(SyncGateMap) == 0 { t.Fatal("SyncGateMap should not be empty") } 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/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/serverdb/apikeys.go b/internal/serverdb/apikeys.go index 7240221c..0f1744d7 100644 --- a/internal/serverdb/apikeys.go +++ b/internal/serverdb/apikeys.go @@ -12,9 +12,9 @@ import ( ) const ( - apiKeyPrefix = "td_live_" - impersonationKeyPrefix = "td_ipk_" - keyLength = 32 + apiKeyPrefix = "td_live_" + impersonationKeyPrefix = "td_ipk_" + keyLength = 32 ) var base62Chars = []byte("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") diff --git a/internal/serverdb/auth_events.go b/internal/serverdb/auth_events.go index 0a0ceb14..89dfa3b2 100644 --- a/internal/serverdb/auth_events.go +++ b/internal/serverdb/auth_events.go @@ -9,22 +9,22 @@ 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. const ( - AuthEventStarted = "started" - AuthEventCodeVerified = "code_verified" - AuthEventKeyIssued = "key_issued" - AuthEventExpired = "expired" - AuthEventFailed = "failed" - AuthEventImpersonationIssued = "impersonation_issued" + AuthEventStarted = "started" + AuthEventCodeVerified = "code_verified" + AuthEventKeyIssued = "key_issued" + AuthEventExpired = "expired" + AuthEventFailed = "failed" + AuthEventImpersonationIssued = "impersonation_issued" ) // InsertAuthEvent inserts an auth event row. 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/applyrepro_test.go b/internal/sync/applyrepro_test.go index 0d7f15b8..67b057e4 100644 --- a/internal/sync/applyrepro_test.go +++ b/internal/sync/applyrepro_test.go @@ -1,123 +1,143 @@ package sync + import ( - "database/sql" - "encoding/json" - "testing" - _ "github.com/mattn/go-sqlite3" + "database/sql" + "encoding/json" + _ "github.com/mattn/go-sqlite3" + "testing" ) + func setupIssuesTable(t *testing.T) *sql.DB { - db, _ := sql.Open("sqlite3", ":memory:") - _, err := db.Exec(`CREATE TABLE issues (id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT DEFAULT '', status TEXT NOT NULL DEFAULT 'open', type TEXT NOT NULL DEFAULT 'task', priority TEXT NOT NULL DEFAULT 'P2', points INTEGER DEFAULT 0, labels TEXT DEFAULT '', parent_id TEXT DEFAULT '', acceptance TEXT DEFAULT '', implementer_session TEXT DEFAULT '', reviewer_session TEXT DEFAULT '', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, closed_at DATETIME, deleted_at DATETIME, minor INTEGER DEFAULT 0, created_branch TEXT DEFAULT '', creator_session TEXT DEFAULT '', sprint TEXT DEFAULT '', defer_until TEXT, due_date TEXT, defer_count INTEGER DEFAULT 0)`) - if err != nil { t.Fatal(err) } - return db + db, _ := sql.Open("sqlite3", ":memory:") + _, err := db.Exec(`CREATE TABLE issues (id TEXT PRIMARY KEY, title TEXT NOT NULL, description TEXT DEFAULT '', status TEXT NOT NULL DEFAULT 'open', type TEXT NOT NULL DEFAULT 'task', priority TEXT NOT NULL DEFAULT 'P2', points INTEGER DEFAULT 0, labels TEXT DEFAULT '', parent_id TEXT DEFAULT '', acceptance TEXT DEFAULT '', implementer_session TEXT DEFAULT '', reviewer_session TEXT DEFAULT '', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, closed_at DATETIME, deleted_at DATETIME, minor INTEGER DEFAULT 0, created_branch TEXT DEFAULT '', creator_session TEXT DEFAULT '', sprint TEXT DEFAULT '', defer_until TEXT, due_date TEXT, defer_count INTEGER DEFAULT 0)`) + if err != nil { + t.Fatal(err) + } + return db } func applyAndGetLabels(t *testing.T, db *sql.DB, id string, rawJSON string) sql.NullString { - tx, _ := db.Begin() - event := Event{ EntityType: "issues", EntityID: id, ActionType: "create", Payload: json.RawMessage(rawJSON) } - _, err := ApplyEvent(tx, event, func(s string) bool { return s == "issues" }) - if err != nil { t.Fatalf("apply: %v", err) } - tx.Commit() - var labels sql.NullString - db.QueryRow("SELECT labels FROM issues WHERE id=?", id).Scan(&labels) - return labels + tx, _ := db.Begin() + event := Event{EntityType: "issues", EntityID: id, ActionType: "create", Payload: json.RawMessage(rawJSON)} + _, err := ApplyEvent(tx, event, func(s string) bool { return s == "issues" }) + if err != nil { + t.Fatalf("apply: %v", err) + } + tx.Commit() + var labels sql.NullString + db.QueryRow("SELECT labels FROM issues WHERE id=?", id).Scan(&labels) + return labels } func TestReproNullLabels_AbsentKey(t *testing.T) { - db := setupIssuesTable(t); defer db.Close() - l := applyAndGetLabels(t, db, "td-a1", `{"id":"td-a1","title":"x"}`) - t.Logf("ABSENT: valid=%v string=%q", l.Valid, l.String) - if !l.Valid { t.Errorf("labels NULL when key absent") } + db := setupIssuesTable(t) + defer db.Close() + l := applyAndGetLabels(t, db, "td-a1", `{"id":"td-a1","title":"x"}`) + t.Logf("ABSENT: valid=%v string=%q", l.Valid, l.String) + if !l.Valid { + t.Errorf("labels NULL when key absent") + } } func TestReproNullLabels_ExplicitNull(t *testing.T) { - db := setupIssuesTable(t); defer db.Close() - l := applyAndGetLabels(t, db, "td-a2", `{"id":"td-a2","title":"x","labels":null}`) - t.Logf("EXPLICIT null: valid=%v string=%q", l.Valid, l.String) - if !l.Valid { t.Errorf("labels NULL when key explicitly null -- THIS IS THE BUG") } + db := setupIssuesTable(t) + defer db.Close() + l := applyAndGetLabels(t, db, "td-a2", `{"id":"td-a2","title":"x","labels":null}`) + t.Logf("EXPLICIT null: valid=%v string=%q", l.Valid, l.String) + if !l.Valid { + t.Errorf("labels NULL when key explicitly null -- THIS IS THE BUG") + } } func TestReproNullLabels_EmptyArray(t *testing.T) { - db := setupIssuesTable(t); defer db.Close() - l := applyAndGetLabels(t, db, "td-a3", `{"id":"td-a3","title":"x","labels":[]}`) - t.Logf("EMPTY arr: valid=%v string=%q", l.Valid, l.String) - if !l.Valid { t.Errorf("labels NULL when array []") } + db := setupIssuesTable(t) + defer db.Close() + l := applyAndGetLabels(t, db, "td-a3", `{"id":"td-a3","title":"x","labels":[]}`) + t.Logf("EMPTY arr: valid=%v string=%q", l.Valid, l.String) + if !l.Valid { + t.Errorf("labels NULL when array []") + } } func TestReproNullLabels_EmptyString(t *testing.T) { - db := setupIssuesTable(t); defer db.Close() - l := applyAndGetLabels(t, db, "td-a4", `{"id":"td-a4","title":"x","labels":""}`) - t.Logf("EMPTY string: valid=%v string=%q", l.Valid, l.String) - if !l.Valid { t.Errorf("labels NULL when string ''") } + db := setupIssuesTable(t) + defer db.Close() + l := applyAndGetLabels(t, db, "td-a4", `{"id":"td-a4","title":"x","labels":""}`) + t.Logf("EMPTY string: valid=%v string=%q", l.Valid, l.String) + if !l.Valid { + t.Errorf("labels NULL when string ''") + } } -// TestReproNullLabels_AllTextDefaultsNull verifies that ALL TEXT DEFAULT '' -// columns on issues get stored as '' (not NULL) when the payload explicitly +// TestReproNullLabels_AllTextDefaultsNull verifies that ALL TEXT DEFAULT "" +// columns on issues get stored as "" (not NULL) when the payload explicitly // sets them to null. This is the systemic fix: any TEXT column declared -// with DEFAULT '' must get '' instead of NULL on the apply path, so readers +// with DEFAULT "" must get "" instead of NULL on the apply path, so readers // that scan into plain string don't crash. func TestReproNullLabels_AllTextDefaultsNull(t *testing.T) { - db := setupIssuesTable(t); defer db.Close() - tx, _ := db.Begin() - payload := `{ + db := setupIssuesTable(t) + defer db.Close() + tx, _ := db.Begin() + payload := `{ "id":"td-a5","title":"x", "description":null,"labels":null,"parent_id":null,"acceptance":null, "implementer_session":null,"reviewer_session":null,"creator_session":null, "created_branch":null,"sprint":null }` - event := Event{ EntityType: "issues", EntityID: "td-a5", ActionType: "create", Payload: json.RawMessage(payload) } - if _, err := ApplyEvent(tx, event, func(s string) bool { return s == "issues" }); err != nil { - t.Fatalf("apply: %v", err) - } - tx.Commit() + event := Event{EntityType: "issues", EntityID: "td-a5", ActionType: "create", Payload: json.RawMessage(payload)} + if _, err := ApplyEvent(tx, event, func(s string) bool { return s == "issues" }); err != nil { + t.Fatalf("apply: %v", err) + } + tx.Commit() - cols := []string{"description", "labels", "parent_id", "acceptance", - "implementer_session", "reviewer_session", "creator_session", - "created_branch", "sprint"} - for _, col := range cols { - var v sql.NullString - q := "SELECT " + col + " FROM issues WHERE id=?" - if err := db.QueryRow(q, "td-a5").Scan(&v); err != nil { - t.Fatalf("scan %s: %v", col, err) - } - if !v.Valid { - t.Errorf("%s stored as NULL (expected '')", col) - } else if v.String != "" { - t.Errorf("%s stored as %q (expected '')", col, v.String) - } - } + cols := []string{"description", "labels", "parent_id", "acceptance", + "implementer_session", "reviewer_session", "creator_session", + "created_branch", "sprint"} + for _, col := range cols { + var v sql.NullString + q := "SELECT " + col + " FROM issues WHERE id=?" + if err := db.QueryRow(q, "td-a5").Scan(&v); err != nil { + t.Fatalf("scan %s: %v", col, err) + } + if !v.Valid { + t.Errorf("%s stored as NULL (expected '')", col) + } else if v.String != "" { + t.Errorf("%s stored as %q (expected '')", col, v.String) + } + } } // TestReproNullLabels_PartialUpdateNull verifies the partial-update apply -// path (applyPartialUpdate) also defaults nil -> '' for TEXT DEFAULT '' +// path (applyPartialUpdate) also defaults nil -> "" for TEXT DEFAULT "" // columns — without this, an update event that sets a field to null would // write NULL and crash readers the same way a create would. func TestReproNullLabels_PartialUpdateNull(t *testing.T) { - db := setupIssuesTable(t); defer db.Close() + db := setupIssuesTable(t) + defer db.Close() - // Seed a row with non-empty values. - tx1, _ := db.Begin() - seed := `{"id":"td-a6","title":"x","labels":"bug","description":"hi"}` - if _, err := ApplyEvent(tx1, Event{EntityType:"issues", EntityID:"td-a6", ActionType:"create", Payload: json.RawMessage(seed)}, func(s string) bool { return s == "issues" }); err != nil { - t.Fatalf("seed apply: %v", err) - } - tx1.Commit() + // Seed a row with non-empty values. + tx1, _ := db.Begin() + seed := `{"id":"td-a6","title":"x","labels":"bug","description":"hi"}` + if _, err := ApplyEvent(tx1, Event{EntityType: "issues", EntityID: "td-a6", ActionType: "create", Payload: json.RawMessage(seed)}, func(s string) bool { return s == "issues" }); err != nil { + t.Fatalf("seed apply: %v", err) + } + tx1.Commit() - // Partial update that explicitly nulls labels + description. - tx2, _ := db.Begin() - prev := json.RawMessage(`{"id":"td-a6","title":"x","labels":"bug","description":"hi"}`) - next := json.RawMessage(`{"id":"td-a6","title":"x","labels":null,"description":null}`) - res, err := applyPartialUpdateEvent(tx2, Event{EntityType:"issues", EntityID:"td-a6", ActionType:"update", Payload: next}, prev) - if err != nil { - t.Fatalf("partial update: %v", err) - } - _ = res - tx2.Commit() + // Partial update that explicitly nulls labels + description. + tx2, _ := db.Begin() + prev := json.RawMessage(`{"id":"td-a6","title":"x","labels":"bug","description":"hi"}`) + next := json.RawMessage(`{"id":"td-a6","title":"x","labels":null,"description":null}`) + res, err := applyPartialUpdateEvent(tx2, Event{EntityType: "issues", EntityID: "td-a6", ActionType: "update", Payload: next}, prev) + if err != nil { + t.Fatalf("partial update: %v", err) + } + _ = res + tx2.Commit() - for _, col := range []string{"labels", "description"} { - var v sql.NullString - if err := db.QueryRow("SELECT "+col+" FROM issues WHERE id=?", "td-a6").Scan(&v); err != nil { - t.Fatalf("scan %s: %v", col, err) - } - if !v.Valid { - t.Errorf("partial update stored %s as NULL (expected '')", col) - } - } + for _, col := range []string{"labels", "description"} { + var v sql.NullString + if err := db.QueryRow("SELECT "+col+" FROM issues WHERE id=?", "td-a6").Scan(&v); err != nil { + t.Fatalf("scan %s: %v", col, err) + } + if !v.Valid { + t.Errorf("partial update stored %s as NULL (expected '')", col) + } + } } diff --git a/internal/sync/client.go b/internal/sync/client.go index 143d47dd..4cfc0d55 100644 --- a/internal/sync/client.go +++ b/internal/sync/client.go @@ -244,11 +244,11 @@ func GetPendingEventsPreserveSession(tx *sql.Tx, deviceID string) ([]Event, erro var events []Event for rows.Next() { var ( - rowid int64 - id sql.NullString - rowSessionID sql.NullString - actionType, entityType, entityID, tsStr string - newDataStr, prevDataStr sql.NullString + rowid int64 + id sql.NullString + rowSessionID sql.NullString + actionType, entityType, entityID, tsStr string + newDataStr, prevDataStr sql.NullString ) if err := rows.Scan(&rowid, &id, &rowSessionID, &actionType, &entityType, &entityID, &newDataStr, &prevDataStr, &tsStr); err != nil { return nil, fmt.Errorf("scan action_log row: %w", err) diff --git a/internal/sync/events.go b/internal/sync/events.go index 6c032714..01221289 100644 --- a/internal/sync/events.go +++ b/internal/sync/events.go @@ -138,7 +138,7 @@ func getTableColumns(tx *sql.Tx, table string) (map[string]bool, error) { } // getTextEmptyDefaultColumns returns the set of TEXT columns declared with -// DEFAULT '' on the given table. Sync payloads may carry these fields as +// DEFAULT "" on the given table. Sync payloads may carry these fields as // JSON null (either because a previous write set them to NULL, or because // the sender serialized an empty pointer/string as null). Binding NULL for // such columns breaks readers that scan into plain `string` — notably @@ -473,8 +473,8 @@ func upsertEntityWithMode(tx *sql.Tx, entityType, entityID string, newData json. // application-level cascade is needed for FK-backed relations. // // The one exception is issues.parent_id: per migration 30's rationale, -// td uses '' (empty string) as the "no parent" sentinel, which is -// incompatible with a schema-level FK (SQLite treats '' as a real value). +// td uses "" (empty string) as the "no parent" sentinel, which is +// incompatible with a schema-level FK (SQLite treats "" as a real value). // That relation has no FK at all, so parent_id cleanup must still happen // here. This was the regression fix from commit baa9b23 (td-4846e6). func deleteEntity(tx *sql.Tx, entityType, entityID string) error { @@ -537,7 +537,7 @@ func buildInsert(fields map[string]any) (cols string, placeholders string, vals // All other array/object fields are stored as JSON strings. // // textEmptyDefaultCols, when non-nil, lists TEXT columns declared with -// DEFAULT '' on this table. Any field present in fields with a nil value +// DEFAULT "" on this table. Any field present in fields with a nil value // whose column is in this set is defaulted to "" — otherwise INSERT binds // NULL, which breaks readers that scan into plain `string` (see // getTextEmptyDefaultColumns for the symptom that motivated this). 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_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)