feat(doctor): add check for stale .beads/mq/ files (bd-dx2dc)
Add bd doctor check that detects legacy gastown merge queue JSON files in .beads/mq/. These files are local-only remnants from the old mrqueue implementation and can safely be deleted since gt done already creates merge-request wisps in beads. - CheckStaleMQFiles() detects .beads/mq/*.json files - FixStaleMQFiles() removes the entire mq directory - Comprehensive tests for check and fix This is the first step toward removing the mrqueue side-channel from gastown. The follow-up convoy will update Refinery/Witness to use beads exclusively. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
e8a4474788
commit
66c5c4d805
@@ -572,6 +572,11 @@ func runDiagnostics(path string) doctorResult {
|
||||
result.Checks = append(result.Checks, persistentMolCheck)
|
||||
// Don't fail overall check for persistent mol issues, just warn
|
||||
|
||||
// Check 26c: Legacy merge queue files (gastown mrqueue remnants)
|
||||
staleMQFilesCheck := convertDoctorCheck(doctor.CheckStaleMQFiles(path))
|
||||
result.Checks = append(result.Checks, staleMQFilesCheck)
|
||||
// Don't fail overall check for legacy MQ files, just warn
|
||||
|
||||
// Check 27: Expired tombstones (maintenance)
|
||||
tombstonesExpiredCheck := convertDoctorCheck(doctor.CheckExpiredTombstones(path))
|
||||
result.Checks = append(result.Checks, tombstonesExpiredCheck)
|
||||
|
||||
@@ -392,3 +392,56 @@ func CheckPersistentMolIssues(path string) DoctorCheck {
|
||||
Category: CategoryMaintenance,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckStaleMQFiles detects legacy .beads/mq/*.json files from gastown.
|
||||
// These files are LOCAL ONLY (not committed) and represent stale merge queue
|
||||
// entries from the old mrqueue implementation. They are safe to delete since
|
||||
// gt done already creates merge-request wisps in beads.
|
||||
func CheckStaleMQFiles(path string) DoctorCheck {
|
||||
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
|
||||
mqDir := filepath.Join(beadsDir, "mq")
|
||||
|
||||
if _, err := os.Stat(mqDir); os.IsNotExist(err) {
|
||||
return DoctorCheck{
|
||||
Name: "Legacy MQ Files",
|
||||
Status: StatusOK,
|
||||
Message: "No legacy merge queue files",
|
||||
Category: CategoryMaintenance,
|
||||
}
|
||||
}
|
||||
|
||||
files, err := filepath.Glob(filepath.Join(mqDir, "*.json"))
|
||||
if err != nil || len(files) == 0 {
|
||||
return DoctorCheck{
|
||||
Name: "Legacy MQ Files",
|
||||
Status: StatusOK,
|
||||
Message: "No legacy merge queue files",
|
||||
Category: CategoryMaintenance,
|
||||
}
|
||||
}
|
||||
|
||||
return DoctorCheck{
|
||||
Name: "Legacy MQ Files",
|
||||
Status: StatusWarning,
|
||||
Message: fmt.Sprintf("%d stale .beads/mq/*.json file(s)", len(files)),
|
||||
Detail: "Legacy gastown merge queue files (local only, safe to delete)",
|
||||
Fix: "Run 'bd doctor --fix' to delete, or 'rm -rf .beads/mq/'",
|
||||
Category: CategoryMaintenance,
|
||||
}
|
||||
}
|
||||
|
||||
// FixStaleMQFiles removes the legacy .beads/mq/ directory and all its contents.
|
||||
func FixStaleMQFiles(path string) error {
|
||||
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
|
||||
mqDir := filepath.Join(beadsDir, "mq")
|
||||
|
||||
if _, err := os.Stat(mqDir); os.IsNotExist(err) {
|
||||
return nil // Nothing to do
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(mqDir); err != nil {
|
||||
return fmt.Errorf("failed to remove %s: %w", mqDir, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -91,3 +91,124 @@ func TestCheckCompactionCandidates_NoDatabase(t *testing.T) {
|
||||
t.Errorf("expected category 'Maintenance', got %q", check.Category)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckStaleMQFiles_NoMQDirectory(t *testing.T) {
|
||||
// Create temp directory with .beads but no mq subdirectory
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .beads dir: %v", err)
|
||||
}
|
||||
|
||||
check := CheckStaleMQFiles(tmpDir)
|
||||
|
||||
if check.Name != "Legacy MQ Files" {
|
||||
t.Errorf("expected name 'Legacy MQ Files', got %q", check.Name)
|
||||
}
|
||||
if check.Status != StatusOK {
|
||||
t.Errorf("expected status OK, got %q", check.Status)
|
||||
}
|
||||
if check.Message != "No legacy merge queue files" {
|
||||
t.Errorf("expected message about no legacy files, got %q", check.Message)
|
||||
}
|
||||
if check.Category != CategoryMaintenance {
|
||||
t.Errorf("expected category 'Maintenance', got %q", check.Category)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckStaleMQFiles_EmptyMQDirectory(t *testing.T) {
|
||||
// Create temp directory with .beads/mq but no JSON files
|
||||
tmpDir := t.TempDir()
|
||||
mqDir := filepath.Join(tmpDir, ".beads", "mq")
|
||||
if err := os.MkdirAll(mqDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .beads/mq dir: %v", err)
|
||||
}
|
||||
|
||||
check := CheckStaleMQFiles(tmpDir)
|
||||
|
||||
if check.Status != StatusOK {
|
||||
t.Errorf("expected status OK for empty mq dir, got %q", check.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckStaleMQFiles_WithJSONFiles(t *testing.T) {
|
||||
// Create temp directory with .beads/mq containing JSON files
|
||||
tmpDir := t.TempDir()
|
||||
mqDir := filepath.Join(tmpDir, ".beads", "mq")
|
||||
if err := os.MkdirAll(mqDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .beads/mq dir: %v", err)
|
||||
}
|
||||
|
||||
// Create some stale MQ files
|
||||
for _, name := range []string{"mr-abc123.json", "mr-def456.json"} {
|
||||
path := filepath.Join(mqDir, name)
|
||||
if err := os.WriteFile(path, []byte(`{"id":"test"}`), 0644); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
check := CheckStaleMQFiles(tmpDir)
|
||||
|
||||
if check.Name != "Legacy MQ Files" {
|
||||
t.Errorf("expected name 'Legacy MQ Files', got %q", check.Name)
|
||||
}
|
||||
if check.Status != StatusWarning {
|
||||
t.Errorf("expected status Warning for mq dir with files, got %q", check.Status)
|
||||
}
|
||||
if check.Message != "2 stale .beads/mq/*.json file(s)" {
|
||||
t.Errorf("expected message about 2 stale files, got %q", check.Message)
|
||||
}
|
||||
if check.Fix == "" {
|
||||
t.Error("expected fix message to be present")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixStaleMQFiles_RemovesDirectory(t *testing.T) {
|
||||
// Create temp directory with .beads/mq containing JSON files
|
||||
tmpDir := t.TempDir()
|
||||
mqDir := filepath.Join(tmpDir, ".beads", "mq")
|
||||
if err := os.MkdirAll(mqDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .beads/mq dir: %v", err)
|
||||
}
|
||||
|
||||
// Create a stale MQ file
|
||||
path := filepath.Join(mqDir, "mr-abc123.json")
|
||||
if err := os.WriteFile(path, []byte(`{"id":"test"}`), 0644); err != nil {
|
||||
t.Fatalf("failed to create file: %v", err)
|
||||
}
|
||||
|
||||
// Verify directory exists before fix
|
||||
if _, err := os.Stat(mqDir); os.IsNotExist(err) {
|
||||
t.Fatal("mq directory should exist before fix")
|
||||
}
|
||||
|
||||
// Apply the fix
|
||||
if err := FixStaleMQFiles(tmpDir); err != nil {
|
||||
t.Fatalf("FixStaleMQFiles failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify directory is removed after fix
|
||||
if _, err := os.Stat(mqDir); !os.IsNotExist(err) {
|
||||
t.Error("mq directory should not exist after fix")
|
||||
}
|
||||
|
||||
// Verify check now passes
|
||||
check := CheckStaleMQFiles(tmpDir)
|
||||
if check.Status != StatusOK {
|
||||
t.Errorf("expected status OK after fix, got %q", check.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixStaleMQFiles_NoDirectory(t *testing.T) {
|
||||
// Create temp directory with .beads but no mq subdirectory
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .beads dir: %v", err)
|
||||
}
|
||||
|
||||
// Fix should succeed even if directory doesn't exist
|
||||
if err := FixStaleMQFiles(tmpDir); err != nil {
|
||||
t.Fatalf("FixStaleMQFiles should not fail when directory doesn't exist: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +310,8 @@ func applyFixList(path string, fixes []doctorCheck) {
|
||||
// No auto-fix: pruning deletes data, must be user-controlled
|
||||
fmt.Printf(" ⚠ Run 'bd cleanup --older-than 90' to prune old closed issues\n")
|
||||
continue
|
||||
case "Legacy MQ Files":
|
||||
err = doctor.FixStaleMQFiles(path)
|
||||
default:
|
||||
fmt.Printf(" ⚠ No automatic fix available for %s\n", check.Name)
|
||||
fmt.Printf(" Manual fix: %s\n", check.Fix)
|
||||
|
||||
Reference in New Issue
Block a user