diff --git a/.beads/beads.jsonl b/.beads/beads.jsonl index c9c5ec47..799d2736 100644 --- a/.beads/beads.jsonl +++ b/.beads/beads.jsonl @@ -565,6 +565,10 @@ {"id":"bd-ola6","content_hash":"79461888e8a7875bf3623b8db44ea004f73a2374daa52ae9cb3fc9d3ce5e6a8b","title":"Implement transaction retry logic for SQLITE_BUSY","description":"BEGIN IMMEDIATE fails immediately on SQLITE_BUSY instead of retrying with exponential backoff.\n\nLocation: internal/storage/sqlite/sqlite.go:223-225\n\nProblem:\n- Under concurrent write load, BEGIN IMMEDIATE can fail with SQLITE_BUSY\n- Current implementation fails immediately instead of retrying\n- Results in spurious failures under normal concurrent usage\n\nSolution: Implement exponential backoff retry:\n- Retry up to N times (e.g., 5)\n- Backoff: 10ms, 20ms, 40ms, 80ms, 160ms\n- Check for context cancellation between retries\n- Only retry on SQLITE_BUSY/database locked errors\n\nImpact: Spurious failures under concurrent write load\n\nEffort: 3 hours","status":"open","priority":1,"issue_type":"feature","created_at":"2025-11-16T14:51:31.247147-08:00","updated_at":"2025-11-16T14:51:31.247147-08:00","source_repo":"."} {"id":"bd-omx1","content_hash":"e61d74adb03fc8275c97242df8ce0e4146db7e49271e4e86c3379b4a3fbab0d8","title":"Add `bd merge` command wrapping 3-way merge logic","description":"Implement CLI command to invoke beads-merge functionality.\n\n**Interface**:\n```bash\nbd merge \u003coutput\u003e \u003cbase\u003e \u003cleft\u003e \u003cright\u003e\nbd merge --debug \u003coutput\u003e \u003cbase\u003e \u003cleft\u003e \u003cright\u003e\n```\n\n**Behavior**:\n- Exit code 0 on clean merge\n- Exit code 1 if conflicts (write conflict markers)\n- Support --debug flag for verbose output\n- Match beads-merge's existing behavior\n\n**File**: `cmd/bd/merge.go`","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-05T18:42:20.427429-08:00","updated_at":"2025-11-05T19:01:29.071365-08:00","closed_at":"2025-11-05T19:01:29.071365-08:00","source_repo":".","dependencies":[{"issue_id":"bd-omx1","depends_on_id":"bd-qqvw","type":"parent-child","created_at":"2025-11-05T18:42:28.709123-08:00","created_by":"daemon"},{"issue_id":"bd-omx1","depends_on_id":"bd-oif6","type":"blocks","created_at":"2025-11-05T18:42:35.436444-08:00","created_by":"daemon"}]} {"id":"bd-p0zr","content_hash":"5e518ce89ce35cb4b5b534b8c1287679b7984bc73f7c6747773962277d2ad1bc","title":"bd message: Improve type safety with typed parameter structs","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-08T12:54:29.675678-08:00","updated_at":"2025-11-08T12:58:59.559643-08:00","closed_at":"2025-11-08T12:58:59.559643-08:00","source_repo":".","dependencies":[{"issue_id":"bd-p0zr","depends_on_id":"bd-6uix","type":"parent-child","created_at":"2025-11-08T12:55:55.058354-08:00","created_by":"daemon"}]} +{"id":"bd-p3b0","content_hash":"f1df1b307f024d6b07eb4a8bc757dbcbce5af4f417f666f42a1b59315c4ec434","title":"Code review bd-loka implementation","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-23T17:10:11.237786-08:00","updated_at":"2025-11-23T17:16:12.294528-08:00","closed_at":"2025-11-23T17:16:12.294528-08:00","source_repo":"."} +{"id":"bd-p3b0.1","content_hash":"67795fe9b992b35691163b5c9a89292248e9c29585cb877fff35ec61434c0f15","title":"Fix variable shadowing in upgradeAckCmd","description":"","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-23T17:12:26.745382-08:00","updated_at":"2025-11-23T17:12:57.792344-08:00","closed_at":"2025-11-23T17:12:57.792344-08:00","source_repo":".","dependencies":[{"issue_id":"bd-p3b0.1","depends_on_id":"bd-p3b0","type":"parent-child","created_at":"2025-11-23T17:12:26.74595-08:00","created_by":"daemon"}]} +{"id":"bd-p3b0.2","content_hash":"f29e689907c8d3539f01a3eafca8af301427ef66c85b67ab78c79e35ca2a8225","title":"Fix inconsistent version comparison logic in trackBdVersion","description":"","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-11-23T17:12:27.953105-08:00","updated_at":"2025-11-23T17:13:28.241231-08:00","closed_at":"2025-11-23T17:13:28.241231-08:00","source_repo":".","dependencies":[{"issue_id":"bd-p3b0.2","depends_on_id":"bd-p3b0","type":"parent-child","created_at":"2025-11-23T17:12:27.953605-08:00","created_by":"daemon"}]} +{"id":"bd-p3b0.3","content_hash":"d452f0b9a6794faa358884d094f89869ec1828fb3e99d20b546e74630e94d9a9","title":"Add unit tests for version tracking functions","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-23T17:12:29.267781-08:00","updated_at":"2025-11-23T17:16:10.649443-08:00","closed_at":"2025-11-23T17:16:10.649443-08:00","source_repo":".","dependencies":[{"issue_id":"bd-p3b0.3","depends_on_id":"bd-p3b0","type":"parent-child","created_at":"2025-11-23T17:12:29.268313-08:00","created_by":"daemon"}]} {"id":"bd-p65x","content_hash":"9fb7f74dbd1c92d47ff34bae3a58b9a4b97643a065cc07e3f76d20537f93be91","title":"Latency test 1","description":"","status":"closed","priority":3,"issue_type":"task","created_at":"2025-11-20T12:09:09.267424-05:00","updated_at":"2025-11-20T12:09:09.267424-05:00","closed_at":"2025-11-08T00:06:46.198388-08:00","source_repo":"."} {"id":"bd-p68x","content_hash":"2adc58598da8443025691815c351057400ddaa6fa6f0121f1dbb85af58d8d6e8","title":"Create examples for common workflows","description":"Add examples/ subdirectories: OSS contributor workflow, team branch workflow, multi-phase development, multiple personas (architect/implementer). Each with README and sample configs.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-05T18:04:30.128257-08:00","updated_at":"2025-11-05T19:27:33.07555-08:00","closed_at":"2025-11-05T19:08:39.035904-08:00","source_repo":".","dependencies":[{"issue_id":"bd-p68x","depends_on_id":"bd-8rd","type":"parent-child","created_at":"2025-11-05T18:04:39.247515-08:00","created_by":"daemon"}]} {"id":"bd-p6vp","content_hash":"1df6d3b9b438cdcdbc618c24fea48769c1f22e8a8701af4e742531d4433ca7ea","title":"Clarify .beads/.gitattributes handling in Protected Branches docs","description":"Protected Branches docs quick start leaves untracked `.beads` directory and `.gitattributes`.\nQuestion: Are these changes meant to be checked into the protected branch?\nNeed to clarify if these should be ignored or committed, or if the instructions are missing a step.\n","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-20T18:56:25.79407-05:00","updated_at":"2025-11-20T18:56:25.79407-05:00","source_repo":"."} diff --git a/cmd/bd/upgrade.go b/cmd/bd/upgrade.go index 9d54c5fd..1fe0d986 100644 --- a/cmd/bd/upgrade.go +++ b/cmd/bd/upgrade.go @@ -169,7 +169,7 @@ Examples: cfg = configfile.DefaultConfig() } - previousVersion := cfg.LastBdVersion + lastSeenVersion := cfg.LastBdVersion cfg.LastBdVersion = Version if err := cfg.Save(beadsDir); err != nil { @@ -185,17 +185,17 @@ Examples: outputJSON(map[string]interface{}{ "acknowledged": true, "current_version": Version, - "previous_version": previousVersion, + "previous_version": lastSeenVersion, }) return } - if previousVersion == Version { + if lastSeenVersion == Version { fmt.Printf("✓ Already on v%s\n", Version) - } else if previousVersion == "" { + } else if lastSeenVersion == "" { fmt.Printf("✓ Acknowledged bd v%s\n", Version) } else { - fmt.Printf("✓ Acknowledged upgrade from v%s to v%s\n", previousVersion, Version) + fmt.Printf("✓ Acknowledged upgrade from v%s to v%s\n", lastSeenVersion, Version) } }, } diff --git a/cmd/bd/version_tracking.go b/cmd/bd/version_tracking.go index 1e76c4ca..c24c918e 100644 --- a/cmd/bd/version_tracking.go +++ b/cmd/bd/version_tracking.go @@ -43,6 +43,7 @@ func trackBdVersion() { // Update metadata.json with current version (best effort) // Only write if version actually changed to minimize I/O + // Also update on first run (when LastBdVersion is empty) to initialize tracking if cfg.LastBdVersion != Version { cfg.LastBdVersion = Version _ = cfg.Save(beadsDir) // Silent failure is fine @@ -52,13 +53,17 @@ func trackBdVersion() { // getVersionsSince returns all version changes since the given version. // If sinceVersion is empty, returns all known versions. // Returns changes in chronological order (oldest first). +// +// Note: versionChanges array is in reverse chronological order (newest first), +// so we return elements before the found index and reverse the slice. func getVersionsSince(sinceVersion string) []VersionChange { if sinceVersion == "" { - // Return all versions + // Return all versions (already in reverse chronological, but kept for compatibility) return versionChanges } // Find the index of sinceVersion + // versionChanges is ordered newest-first: [0.23.0, 0.22.1, 0.22.0, 0.21.0] startIdx := -1 for i, vc := range versionChanges { if vc.Version == sinceVersion { @@ -73,13 +78,22 @@ func getVersionsSince(sinceVersion string) []VersionChange { return versionChanges } - // Return versions after sinceVersion (don't include sinceVersion itself) - if startIdx+1 < len(versionChanges) { - return versionChanges[startIdx+1:] + if startIdx == 0 { + // Already on the newest version + return []VersionChange{} } - // No new versions - return []VersionChange{} + // Return versions before sinceVersion (those are newer) + // Then reverse to get chronological order (oldest first) + newerVersions := versionChanges[:startIdx] + + // Reverse the slice to get chronological order + result := make([]VersionChange, len(newerVersions)) + for i := range newerVersions { + result[i] = newerVersions[len(newerVersions)-1-i] + } + + return result } // maybeShowUpgradeNotification displays a one-time upgrade notification if version changed. diff --git a/cmd/bd/version_tracking_test.go b/cmd/bd/version_tracking_test.go new file mode 100644 index 00000000..06c02419 --- /dev/null +++ b/cmd/bd/version_tracking_test.go @@ -0,0 +1,310 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "github.com/steveyegge/beads/internal/configfile" +) + +func TestGetVersionsSince(t *testing.T) { + tests := []struct { + name string + sinceVersion string + expectedCount int + description string + }{ + { + name: "empty version returns all", + sinceVersion: "", + expectedCount: len(versionChanges), + description: "Should return all versions when sinceVersion is empty", + }, + { + name: "version not in changelog", + sinceVersion: "0.1.0", + expectedCount: len(versionChanges), + description: "Should return all versions when sinceVersion not found", + }, + { + name: "oldest version in changelog", + sinceVersion: "0.21.0", + expectedCount: 3, // 0.22.0, 0.22.1, 0.23.0 + description: "Should return versions newer than oldest", + }, + { + name: "middle version returns newer versions", + sinceVersion: "0.22.0", + expectedCount: 2, // 0.22.1 and 0.23.0 + description: "Should return versions newer than specified", + }, + { + name: "latest version returns empty", + sinceVersion: "0.23.0", + expectedCount: 0, + description: "Should return empty slice when already on latest in changelog", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getVersionsSince(tt.sinceVersion) + if len(result) != tt.expectedCount { + t.Errorf("getVersionsSince(%q) returned %d versions, want %d: %s", + tt.sinceVersion, len(result), tt.expectedCount, tt.description) + } + }) + } +} + +func TestGetVersionsSinceOrder(t *testing.T) { + // Test that versions are returned in chronological order (oldest first) + // versionChanges array is newest-first, but getVersionsSince returns oldest-first + result := getVersionsSince("0.21.0") + + if len(result) != 3 { + t.Fatalf("Expected 3 versions after 0.21.0, got %d", len(result)) + } + + // Verify chronological order by checking dates increase + // result should be [0.22.0, 0.22.1, 0.23.0] + for i := 1; i < len(result); i++ { + prev := result[i-1] + curr := result[i] + + // Simple date comparison (YYYY-MM-DD format) + if curr.Date < prev.Date { + t.Errorf("Versions not in chronological order: %s (%s) should come before %s (%s)", + prev.Version, prev.Date, curr.Version, curr.Date) + } + } + + // Check specific order + expectedVersions := []string{"0.22.0", "0.22.1", "0.23.0"} + for i, expected := range expectedVersions { + if result[i].Version != expected { + t.Errorf("Version at index %d = %s, want %s", i, result[i].Version, expected) + } + } +} + +func TestTrackBdVersion_NoBeadsDir(t *testing.T) { + // Save original state + origUpgradeDetected := versionUpgradeDetected + origPreviousVersion := previousVersion + defer func() { + versionUpgradeDetected = origUpgradeDetected + previousVersion = origPreviousVersion + }() + + // Change to temp directory with no .beads + tmpDir := t.TempDir() + origWd, _ := os.Getwd() + defer os.Chdir(origWd) + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to change to temp dir: %v", err) + } + + // trackBdVersion should silently succeed + trackBdVersion() + + // Should not detect upgrade when no .beads dir exists + if versionUpgradeDetected { + t.Error("Expected no upgrade detection when .beads directory doesn't exist") + } +} + +func TestTrackBdVersion_FirstRun(t *testing.T) { + // Create temp .beads directory + tmpDir := t.TempDir() + beadsDir := filepath.Join(tmpDir, ".beads") + if err := os.MkdirAll(beadsDir, 0755); err != nil { + t.Fatalf("Failed to create .beads: %v", err) + } + + // Change to temp directory + origWd, _ := os.Getwd() + defer os.Chdir(origWd) + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to change to temp dir: %v", err) + } + + // Save original state + origUpgradeDetected := versionUpgradeDetected + origPreviousVersion := previousVersion + defer func() { + versionUpgradeDetected = origUpgradeDetected + previousVersion = origPreviousVersion + }() + + // Reset state + versionUpgradeDetected = false + previousVersion = "" + + // trackBdVersion should create metadata.json + trackBdVersion() + + // Should not detect upgrade on first run + if versionUpgradeDetected { + t.Error("Expected no upgrade detection on first run") + } + + // Should have created metadata.json with current version + cfg, err := configfile.Load(beadsDir) + if err != nil { + t.Fatalf("Failed to load config after tracking: %v", err) + } + if cfg.LastBdVersion != Version { + t.Errorf("LastBdVersion = %q, want %q", cfg.LastBdVersion, Version) + } +} + +func TestTrackBdVersion_UpgradeDetection(t *testing.T) { + // Create temp .beads directory + tmpDir := t.TempDir() + beadsDir := filepath.Join(tmpDir, ".beads") + if err := os.MkdirAll(beadsDir, 0755); err != nil { + t.Fatalf("Failed to create .beads: %v", err) + } + + // Change to temp directory + origWd, _ := os.Getwd() + defer os.Chdir(origWd) + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to change to temp dir: %v", err) + } + + // Create metadata.json with old version + cfg := configfile.DefaultConfig() + cfg.LastBdVersion = "0.22.0" + if err := cfg.Save(beadsDir); err != nil { + t.Fatalf("Failed to save config: %v", err) + } + + // Save original state + origUpgradeDetected := versionUpgradeDetected + origPreviousVersion := previousVersion + defer func() { + versionUpgradeDetected = origUpgradeDetected + previousVersion = origPreviousVersion + }() + + // Reset state + versionUpgradeDetected = false + previousVersion = "" + + // trackBdVersion should detect upgrade + trackBdVersion() + + // Should detect upgrade + if !versionUpgradeDetected { + t.Error("Expected upgrade detection when version changed") + } + + if previousVersion != "0.22.0" { + t.Errorf("previousVersion = %q, want %q", previousVersion, "0.22.0") + } + + // Should have updated metadata.json to current version + cfg, err := configfile.Load(beadsDir) + if err != nil { + t.Fatalf("Failed to load config after tracking: %v", err) + } + if cfg.LastBdVersion != Version { + t.Errorf("LastBdVersion = %q, want %q", cfg.LastBdVersion, Version) + } +} + +func TestTrackBdVersion_SameVersion(t *testing.T) { + // Create temp .beads directory + tmpDir := t.TempDir() + beadsDir := filepath.Join(tmpDir, ".beads") + if err := os.MkdirAll(beadsDir, 0755); err != nil { + t.Fatalf("Failed to create .beads: %v", err) + } + + // Change to temp directory + origWd, _ := os.Getwd() + defer os.Chdir(origWd) + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to change to temp dir: %v", err) + } + + // Create metadata.json with current version + cfg := configfile.DefaultConfig() + cfg.LastBdVersion = Version + if err := cfg.Save(beadsDir); err != nil { + t.Fatalf("Failed to save config: %v", err) + } + + // Save original state + origUpgradeDetected := versionUpgradeDetected + origPreviousVersion := previousVersion + defer func() { + versionUpgradeDetected = origUpgradeDetected + previousVersion = origPreviousVersion + }() + + // Reset state + versionUpgradeDetected = false + previousVersion = "" + + // trackBdVersion should not detect upgrade + trackBdVersion() + + // Should not detect upgrade + if versionUpgradeDetected { + t.Error("Expected no upgrade detection when version is the same") + } +} + +func TestMaybeShowUpgradeNotification(t *testing.T) { + // Save original state + origUpgradeDetected := versionUpgradeDetected + origPreviousVersion := previousVersion + origUpgradeAcknowledged := upgradeAcknowledged + defer func() { + versionUpgradeDetected = origUpgradeDetected + previousVersion = origPreviousVersion + upgradeAcknowledged = origUpgradeAcknowledged + }() + + // Test: No upgrade detected + versionUpgradeDetected = false + upgradeAcknowledged = false + previousVersion = "" + + if maybeShowUpgradeNotification() { + t.Error("Should not show notification when no upgrade detected") + } + + // Test: Upgrade detected but already acknowledged + versionUpgradeDetected = true + upgradeAcknowledged = true + previousVersion = "0.22.0" + + if maybeShowUpgradeNotification() { + t.Error("Should not show notification when already acknowledged") + } + + // Test: Upgrade detected and not acknowledged + versionUpgradeDetected = true + upgradeAcknowledged = false + previousVersion = "0.22.0" + + if !maybeShowUpgradeNotification() { + t.Error("Should show notification when upgrade detected and not acknowledged") + } + + // Should be marked as acknowledged after showing + if !upgradeAcknowledged { + t.Error("Should mark as acknowledged after showing notification") + } + + // Calling again should not show (already acknowledged) + if maybeShowUpgradeNotification() { + t.Error("Should not show notification twice") + } +}