Files
gastown/internal/doctor/rig_routes_jsonl_check_test.go
Julian Knutsen 043a6abc59 fix(beads): prevent routes.jsonl corruption and add doctor check for rig-level routes.jsonl (#377)
* 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>
2026-01-12 01:45:26 -08:00

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)
}
})
}