feat(doctor): add --dry-run flag and fix registry parsing (bd-qn5, bd-a5z)

- Add --dry-run flag to preview fixes without applying changes
- Handle corrupted/empty/null-byte registry files gracefully
- Treat corrupted registry as empty instead of failing

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-03 00:09:25 -08:00
parent 18191f5e54
commit cbf48672e7
3 changed files with 99 additions and 8 deletions

View File

@@ -71,6 +71,7 @@ func (r *Registry) withFileLock(fn func() error) error {
// readEntriesLocked reads all entries from the registry file.
// Caller must hold the file lock.
// bd-qn5: Handles missing, empty, or corrupted registry files gracefully.
func (r *Registry) readEntriesLocked() ([]RegistryEntry, error) {
data, err := os.ReadFile(r.path)
if err != nil {
@@ -80,9 +81,23 @@ func (r *Registry) readEntriesLocked() ([]RegistryEntry, error) {
return nil, fmt.Errorf("failed to read registry: %w", err)
}
// bd-qn5: Handle empty file or file with only whitespace/null bytes
// This can happen if the file was created but never written to, or was corrupted
trimmed := make([]byte, 0, len(data))
for _, b := range data {
if b != 0 && b != ' ' && b != '\t' && b != '\n' && b != '\r' {
trimmed = append(trimmed, b)
}
}
if len(trimmed) == 0 {
return []RegistryEntry{}, nil
}
var entries []RegistryEntry
if err := json.Unmarshal(data, &entries); err != nil {
return nil, fmt.Errorf("failed to parse registry: %w", err)
// bd-qn5: If registry is corrupted, treat as empty rather than failing
// A corrupted registry just means we'll need to rediscover daemons
return []RegistryEntry{}, nil
}
return entries, nil

View File

@@ -240,13 +240,46 @@ func TestRegistryCorruptedFile(t *testing.T) {
os.MkdirAll(filepath.Dir(registryPath), 0755)
os.WriteFile(registryPath, []byte("invalid json{{{"), 0644)
// Reading should return an error
// bd-qn5: Corrupted registry should be treated as empty, not an error
// This allows bd doctor to work gracefully when registry is corrupted
entries, err := registry.readEntries()
if err == nil {
t.Error("Expected error when reading corrupted registry")
if err != nil {
t.Errorf("Expected corrupted registry to be treated as empty, got error: %v", err)
}
if entries != nil {
t.Errorf("Expected nil entries on error, got %v", entries)
if len(entries) != 0 {
t.Errorf("Expected empty entries for corrupted registry, got %v", entries)
}
}
// bd-qn5: Test for the specific null bytes case that was reported
func TestRegistryNullBytesFile(t *testing.T) {
tmpDir := t.TempDir()
registryPath := filepath.Join(tmpDir, ".beads", "registry.json")
homeEnv := "HOME"
if runtime.GOOS == "windows" {
homeEnv = "USERPROFILE"
}
oldHome := os.Getenv(homeEnv)
os.Setenv(homeEnv, tmpDir)
defer os.Setenv(homeEnv, oldHome)
registry, err := NewRegistry()
if err != nil {
t.Fatalf("Failed to create registry: %v", err)
}
// Create a file with null bytes (the reported error case)
os.MkdirAll(filepath.Dir(registryPath), 0755)
os.WriteFile(registryPath, []byte{0, 0, 0, 0}, 0644)
// Should treat as empty, not error
entries, err := registry.readEntries()
if err != nil {
t.Errorf("Expected null-byte registry to be treated as empty, got error: %v", err)
}
if len(entries) != 0 {
t.Errorf("Expected empty entries for null-byte registry, got %v", entries)
}
}