fix(doctor): add beads database check to detect empty issues.db

Add a new doctor check that detects when issues.db is empty but
issues.jsonl has content. This situation can cause "table issues has
no column named pinned" errors when running bd mail send.

The check:
- Detects empty database file alongside non-empty JSONL
- Can auto-fix by deleting the empty database and triggering rebuild
- Works for both town-level and rig-level beads

Run 'gt doctor --fix' to automatically fix this issue.

Fixes gt-bxi8

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-19 16:06:12 -08:00
parent 349205eda1
commit debe47cc62
4 changed files with 459 additions and 183 deletions

View File

@@ -0,0 +1,101 @@
package doctor
import (
"os"
"path/filepath"
"testing"
)
func TestNewBeadsDatabaseCheck(t *testing.T) {
check := NewBeadsDatabaseCheck()
if check.Name() != "beads-database" {
t.Errorf("expected name 'beads-database', got %q", check.Name())
}
if !check.CanFix() {
t.Error("expected CanFix to return true")
}
}
func TestBeadsDatabaseCheck_NoBeadsDir(t *testing.T) {
tmpDir := t.TempDir()
check := NewBeadsDatabaseCheck()
ctx := &CheckContext{TownRoot: tmpDir}
result := check.Run(ctx)
if result.Status != StatusWarning {
t.Errorf("expected StatusWarning, got %v", result.Status)
}
}
func TestBeadsDatabaseCheck_NoDatabase(t *testing.T) {
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatal(err)
}
check := NewBeadsDatabaseCheck()
ctx := &CheckContext{TownRoot: tmpDir}
result := check.Run(ctx)
if result.Status != StatusOK {
t.Errorf("expected StatusOK, got %v", result.Status)
}
}
func TestBeadsDatabaseCheck_EmptyDatabase(t *testing.T) {
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatal(err)
}
// Create empty database
dbPath := filepath.Join(beadsDir, "issues.db")
if err := os.WriteFile(dbPath, []byte{}, 0644); err != nil {
t.Fatal(err)
}
// Create JSONL with content
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
if err := os.WriteFile(jsonlPath, []byte(`{"id":"test-1","title":"Test"}`), 0644); err != nil {
t.Fatal(err)
}
check := NewBeadsDatabaseCheck()
ctx := &CheckContext{TownRoot: tmpDir}
result := check.Run(ctx)
if result.Status != StatusError {
t.Errorf("expected StatusError for empty db with content in jsonl, got %v", result.Status)
}
}
func TestBeadsDatabaseCheck_PopulatedDatabase(t *testing.T) {
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatal(err)
}
// Create database with content
dbPath := filepath.Join(beadsDir, "issues.db")
if err := os.WriteFile(dbPath, []byte("SQLite format 3"), 0644); err != nil {
t.Fatal(err)
}
check := NewBeadsDatabaseCheck()
ctx := &CheckContext{TownRoot: tmpDir}
result := check.Run(ctx)
if result.Status != StatusOK {
t.Errorf("expected StatusOK for populated db, got %v", result.Status)
}
}