fix(bd-68e4): make DBJSONLSync bidirectional - export DB when it has more issues

- Modified fix.DBJSONLSync() to detect which direction to sync:
  - If DB > JSONL: run 'bd export' to sync JSONL (DB has newer data)
  - If JSONL > DB: run 'bd sync --import-only' to import (JSONL is source of truth)
  - If equal but different timestamps: use file mtime to decide direction

- Updated CheckDatabaseJSONLSync() error messages to recommend correct fix direction:
  - Shows different guidance based on whether DB or JSONL has more issues

- Added helper functions:
  - countDatabaseIssues() to count issues in SQLite
  - countJSONLIssues() to count issues in JSONL (local, avoids circular import)

- Added tests for countJSONLIssues() with edge cases

Fixes issue where 'bd doctor --fix' would recommend 'bd sync --import-only'
when DB > JSONL, which would be a no-op since JSONL hasn't changed.
This commit is contained in:
matt wilkie
2025-12-21 11:22:37 -07:00
parent fa90c95475
commit 2de4d0facd
4 changed files with 356 additions and 186 deletions

View File

@@ -628,3 +628,83 @@ func TestSyncBranchConfig_InvalidRemoteURL(t *testing.T) {
}
}
func TestCountJSONLIssues(t *testing.T) {
t.Parallel()
t.Run("empty_JSONL", func(t *testing.T) {
dir := setupTestWorkspace(t)
jsonlPath := filepath.Join(dir, ".beads", "issues.jsonl")
// Create empty JSONL
if err := os.WriteFile(jsonlPath, []byte(""), 0644); err != nil {
t.Fatalf("failed to create JSONL: %v", err)
}
count, err := countJSONLIssues(jsonlPath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if count != 0 {
t.Errorf("expected 0, got %d", count)
}
})
t.Run("valid_issues", func(t *testing.T) {
dir := setupTestWorkspace(t)
jsonlPath := filepath.Join(dir, ".beads", "issues.jsonl")
// Create JSONL with 3 issues
jsonl := []byte(`{"id":"bd-1","title":"First"}
{"id":"bd-2","title":"Second"}
{"id":"bd-3","title":"Third"}
`)
if err := os.WriteFile(jsonlPath, jsonl, 0644); err != nil {
t.Fatalf("failed to create JSONL: %v", err)
}
count, err := countJSONLIssues(jsonlPath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if count != 3 {
t.Errorf("expected 3, got %d", count)
}
})
t.Run("mixed_valid_and_invalid", func(t *testing.T) {
dir := setupTestWorkspace(t)
jsonlPath := filepath.Join(dir, ".beads", "issues.jsonl")
// Create JSONL with 2 valid and some invalid lines
jsonl := []byte(`{"id":"bd-1","title":"First"}
invalid json line
{"id":"bd-2","title":"Second"}
{"title":"No ID"}
`)
if err := os.WriteFile(jsonlPath, jsonl, 0644); err != nil {
t.Fatalf("failed to create JSONL: %v", err)
}
count, err := countJSONLIssues(jsonlPath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if count != 2 {
t.Errorf("expected 2, got %d", count)
}
})
t.Run("nonexistent_file", func(t *testing.T) {
dir := setupTestWorkspace(t)
jsonlPath := filepath.Join(dir, ".beads", "nonexistent.jsonl")
count, err := countJSONLIssues(jsonlPath)
if err == nil {
t.Error("expected error for nonexistent file")
}
if count != 0 {
t.Errorf("expected 0, got %d", count)
}
})
}