* fix(beads): prevent routes.jsonl corruption from bd auto-export When issues.jsonl doesn't exist, bd's auto-export mechanism writes issue data to routes.jsonl, corrupting the routing configuration. Changes: - install.go: Create issues.jsonl before routes.jsonl at town level - manager.go: Create issues.jsonl in rig beads; don't create routes.jsonl (rig-level routes.jsonl breaks bd's walk-up routing to town routes) - Add integration tests for routes.jsonl corruption prevention Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(doctor): add check to detect and fix rig-level routes.jsonl Add RigRoutesJSONLCheck to detect routes.jsonl files in rig .beads directories. These files break bd's walk-up routing to town-level routes.jsonl, causing cross-rig routing failures. The fix unconditionally deletes rig-level routes.jsonl files since bd will auto-export to issues.jsonl on next run. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(rig): add verification that routes.jsonl does NOT exist in rig .beads Add explicit test assertion and detailed comment explaining why rig-level routes.jsonl files must not exist (breaks bd walk-up routing to town routes). Also verify that issues.jsonl DOES exist (prevents bd auto-export corruption). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(doctor): ensure town root route exists in routes.jsonl The RoutesCheck now detects and fixes missing town root routes (hq- -> .). This can happen when routes.jsonl is corrupted or was created without the town route during initialization. Changes: - Detect missing hq- route in Run() - Add hq- route in Fix() when missing - Handle case where routes.jsonl is corrupted (regenerate with town route) - Add comprehensive unit tests for route detection and fixing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(beads): fix routing integration test for routes.jsonl corruption The TestBeadsRoutingFromTownRoot test was failing because bd's auto-export mechanism writes issue data to routes.jsonl when issues.jsonl doesn't exist. This corrupts the routing configuration. Fix: Create empty issues.jsonl after bd init to prevent corruption. This mirrors what gt install does to prevent the same bug. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: julianknutsen <julianknutsen@users.noreply.github> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
207 lines
5.7 KiB
Go
207 lines
5.7 KiB
Go
package doctor
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestRigRoutesJSONLCheck_Run(t *testing.T) {
|
|
t.Run("no rigs returns OK", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
// Create minimal town structure
|
|
if err := os.MkdirAll(filepath.Join(tmpDir, "mayor"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewRigRoutesJSONLCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
result := check.Run(ctx)
|
|
|
|
if result.Status != StatusOK {
|
|
t.Errorf("expected StatusOK, got %v: %s", result.Status, result.Message)
|
|
}
|
|
})
|
|
|
|
t.Run("rig without routes.jsonl returns OK", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
// Create rig with .beads but no routes.jsonl
|
|
rigBeads := filepath.Join(tmpDir, "myrig", ".beads")
|
|
if err := os.MkdirAll(rigBeads, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewRigRoutesJSONLCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
result := check.Run(ctx)
|
|
|
|
if result.Status != StatusOK {
|
|
t.Errorf("expected StatusOK, got %v: %s", result.Status, result.Message)
|
|
}
|
|
})
|
|
|
|
t.Run("rig with routes.jsonl warns", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
rigBeads := filepath.Join(tmpDir, "myrig", ".beads")
|
|
if err := os.MkdirAll(rigBeads, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create routes.jsonl (any content - will be deleted)
|
|
if err := os.WriteFile(filepath.Join(rigBeads, "routes.jsonl"), []byte(`{"prefix":"x-","path":"."}`+"\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewRigRoutesJSONLCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
result := check.Run(ctx)
|
|
|
|
if result.Status != StatusWarning {
|
|
t.Errorf("expected StatusWarning, got %v: %s", result.Status, result.Message)
|
|
}
|
|
if len(result.Details) == 0 {
|
|
t.Error("expected details about the issue")
|
|
}
|
|
})
|
|
|
|
t.Run("multiple rigs with routes.jsonl reports all", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create two rigs with routes.jsonl
|
|
for _, rigName := range []string{"rig1", "rig2"} {
|
|
rigBeads := filepath.Join(tmpDir, rigName, ".beads")
|
|
if err := os.MkdirAll(rigBeads, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(rigBeads, "routes.jsonl"), []byte(`{"prefix":"x-","path":"."}`+"\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
check := NewRigRoutesJSONLCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
result := check.Run(ctx)
|
|
|
|
if result.Status != StatusWarning {
|
|
t.Errorf("expected StatusWarning, got %v", result.Status)
|
|
}
|
|
if len(result.Details) != 2 {
|
|
t.Errorf("expected 2 details, got %d: %v", len(result.Details), result.Details)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRigRoutesJSONLCheck_Fix(t *testing.T) {
|
|
t.Run("deletes routes.jsonl unconditionally", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
rigBeads := filepath.Join(tmpDir, "myrig", ".beads")
|
|
if err := os.MkdirAll(rigBeads, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create routes.jsonl with any content
|
|
routesPath := filepath.Join(rigBeads, "routes.jsonl")
|
|
if err := os.WriteFile(routesPath, []byte(`{"id":"test-abc123","title":"Test Issue"}`+"\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewRigRoutesJSONLCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
|
|
// Run check first to populate affectedRigs
|
|
result := check.Run(ctx)
|
|
if result.Status != StatusWarning {
|
|
t.Fatalf("expected StatusWarning, got %v", result.Status)
|
|
}
|
|
|
|
// Fix
|
|
if err := check.Fix(ctx); err != nil {
|
|
t.Fatalf("Fix() error: %v", err)
|
|
}
|
|
|
|
// Verify routes.jsonl is gone
|
|
if _, err := os.Stat(routesPath); !os.IsNotExist(err) {
|
|
t.Error("routes.jsonl should have been deleted")
|
|
}
|
|
})
|
|
|
|
t.Run("fix is idempotent", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
rigBeads := filepath.Join(tmpDir, "myrig", ".beads")
|
|
if err := os.MkdirAll(rigBeads, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewRigRoutesJSONLCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
|
|
// First run - should pass (no routes.jsonl)
|
|
result := check.Run(ctx)
|
|
if result.Status != StatusOK {
|
|
t.Fatalf("expected StatusOK, got %v", result.Status)
|
|
}
|
|
|
|
// Fix should be no-op
|
|
if err := check.Fix(ctx); err != nil {
|
|
t.Fatalf("Fix() error on clean state: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRigRoutesJSONLCheck_FindRigDirectories(t *testing.T) {
|
|
t.Run("finds rigs from multiple sources", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create mayor directory
|
|
if err := os.MkdirAll(filepath.Join(tmpDir, "mayor"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create town-level .beads with routes.jsonl
|
|
townBeads := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(townBeads, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
routes := `{"prefix":"rig1-","path":"rig1/mayor/rig"}` + "\n"
|
|
if err := os.WriteFile(filepath.Join(townBeads, "routes.jsonl"), []byte(routes), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create rig1 (from routes.jsonl)
|
|
if err := os.MkdirAll(filepath.Join(tmpDir, "rig1", ".beads"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create rig2 (unregistered but has .beads)
|
|
if err := os.MkdirAll(filepath.Join(tmpDir, "rig2", ".beads"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewRigRoutesJSONLCheck()
|
|
rigs := check.findRigDirectories(tmpDir)
|
|
|
|
if len(rigs) != 2 {
|
|
t.Errorf("expected 2 rigs, got %d: %v", len(rigs), rigs)
|
|
}
|
|
})
|
|
|
|
t.Run("excludes mayor and .beads directories", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create directories that should be excluded
|
|
if err := os.MkdirAll(filepath.Join(tmpDir, "mayor", ".beads"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(tmpDir, ".beads"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewRigRoutesJSONLCheck()
|
|
rigs := check.findRigDirectories(tmpDir)
|
|
|
|
if len(rigs) != 0 {
|
|
t.Errorf("expected 0 rigs (mayor and .beads should be excluded), got %d: %v", len(rigs), rigs)
|
|
}
|
|
})
|
|
}
|