Add a new 'prefix-mismatch' check to gt doctor that detects when the prefix configured in rigs.json differs from what routes.jsonl actually uses for a rig's beads. This can happen when: - deriveBeadsPrefix() generates a different prefix than what's in the DB - Someone manually edited rigs.json with the wrong prefix - Beads were initialized before auto-derive existed with a different prefix The check is fixable: running 'gt doctor --fix' will update rigs.json to match the actual prefixes from routes.jsonl. Includes comprehensive tests for: - No routes (nothing to check) - No rigs.json (nothing to check) - Matching prefixes (OK) - Mismatched prefixes (Warning) - Fix functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
318 lines
7.8 KiB
Go
318 lines
7.8 KiB
Go
package doctor
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestNewBeadsDatabaseCheck(t *testing.T) {
|
|
check := NewBeadsDatabaseCheck()
|
|
|
|
if check.Name() != "beads-database" {
|
|
t.Errorf("expected name 'beads-database', got %q", check.Name())
|
|
}
|
|
|
|
if !check.CanFix() {
|
|
t.Error("expected CanFix to return true")
|
|
}
|
|
}
|
|
|
|
func TestBeadsDatabaseCheck_NoBeadsDir(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
check := NewBeadsDatabaseCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
|
|
result := check.Run(ctx)
|
|
|
|
if result.Status != StatusWarning {
|
|
t.Errorf("expected StatusWarning, got %v", result.Status)
|
|
}
|
|
}
|
|
|
|
func TestBeadsDatabaseCheck_NoDatabase(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewBeadsDatabaseCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
|
|
result := check.Run(ctx)
|
|
|
|
if result.Status != StatusOK {
|
|
t.Errorf("expected StatusOK, got %v", result.Status)
|
|
}
|
|
}
|
|
|
|
func TestBeadsDatabaseCheck_EmptyDatabase(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create empty database
|
|
dbPath := filepath.Join(beadsDir, "issues.db")
|
|
if err := os.WriteFile(dbPath, []byte{}, 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create JSONL with content
|
|
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
|
if err := os.WriteFile(jsonlPath, []byte(`{"id":"test-1","title":"Test"}`), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewBeadsDatabaseCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
|
|
result := check.Run(ctx)
|
|
|
|
if result.Status != StatusError {
|
|
t.Errorf("expected StatusError for empty db with content in jsonl, got %v", result.Status)
|
|
}
|
|
}
|
|
|
|
func TestBeadsDatabaseCheck_PopulatedDatabase(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create database with content
|
|
dbPath := filepath.Join(beadsDir, "issues.db")
|
|
if err := os.WriteFile(dbPath, []byte("SQLite format 3"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewBeadsDatabaseCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
|
|
result := check.Run(ctx)
|
|
|
|
if result.Status != StatusOK {
|
|
t.Errorf("expected StatusOK for populated db, got %v", result.Status)
|
|
}
|
|
}
|
|
|
|
func TestNewPrefixMismatchCheck(t *testing.T) {
|
|
check := NewPrefixMismatchCheck()
|
|
|
|
if check.Name() != "prefix-mismatch" {
|
|
t.Errorf("expected name 'prefix-mismatch', got %q", check.Name())
|
|
}
|
|
|
|
if !check.CanFix() {
|
|
t.Error("expected CanFix to return true")
|
|
}
|
|
}
|
|
|
|
func TestPrefixMismatchCheck_NoRoutes(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewPrefixMismatchCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
|
|
result := check.Run(ctx)
|
|
|
|
if result.Status != StatusOK {
|
|
t.Errorf("expected StatusOK for no routes, got %v", result.Status)
|
|
}
|
|
}
|
|
|
|
func TestPrefixMismatchCheck_NoRigsJson(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create routes.jsonl
|
|
routesPath := filepath.Join(beadsDir, "routes.jsonl")
|
|
routesContent := `{"prefix":"gt-","path":"gastown/mayor/rig"}`
|
|
if err := os.WriteFile(routesPath, []byte(routesContent), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewPrefixMismatchCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
|
|
result := check.Run(ctx)
|
|
|
|
if result.Status != StatusOK {
|
|
t.Errorf("expected StatusOK when no rigs.json, got %v", result.Status)
|
|
}
|
|
}
|
|
|
|
func TestPrefixMismatchCheck_Matching(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
mayorDir := filepath.Join(tmpDir, "mayor")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(mayorDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create routes.jsonl with gt- prefix
|
|
routesPath := filepath.Join(beadsDir, "routes.jsonl")
|
|
routesContent := `{"prefix":"gt-","path":"gastown/mayor/rig"}`
|
|
if err := os.WriteFile(routesPath, []byte(routesContent), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create rigs.json with matching gt prefix
|
|
rigsPath := filepath.Join(mayorDir, "rigs.json")
|
|
rigsContent := `{
|
|
"version": 1,
|
|
"rigs": {
|
|
"gastown": {
|
|
"git_url": "https://github.com/example/gastown",
|
|
"beads": {
|
|
"prefix": "gt"
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
if err := os.WriteFile(rigsPath, []byte(rigsContent), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewPrefixMismatchCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
|
|
result := check.Run(ctx)
|
|
|
|
if result.Status != StatusOK {
|
|
t.Errorf("expected StatusOK for matching prefixes, got %v: %s", result.Status, result.Message)
|
|
}
|
|
}
|
|
|
|
func TestPrefixMismatchCheck_Mismatch(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
mayorDir := filepath.Join(tmpDir, "mayor")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(mayorDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create routes.jsonl with gt- prefix
|
|
routesPath := filepath.Join(beadsDir, "routes.jsonl")
|
|
routesContent := `{"prefix":"gt-","path":"gastown/mayor/rig"}`
|
|
if err := os.WriteFile(routesPath, []byte(routesContent), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create rigs.json with WRONG prefix (ga instead of gt)
|
|
rigsPath := filepath.Join(mayorDir, "rigs.json")
|
|
rigsContent := `{
|
|
"version": 1,
|
|
"rigs": {
|
|
"gastown": {
|
|
"git_url": "https://github.com/example/gastown",
|
|
"beads": {
|
|
"prefix": "ga"
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
if err := os.WriteFile(rigsPath, []byte(rigsContent), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewPrefixMismatchCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
|
|
result := check.Run(ctx)
|
|
|
|
if result.Status != StatusWarning {
|
|
t.Errorf("expected StatusWarning for prefix mismatch, got %v: %s", result.Status, result.Message)
|
|
}
|
|
|
|
if len(result.Details) != 1 {
|
|
t.Errorf("expected 1 detail, got %d", len(result.Details))
|
|
}
|
|
}
|
|
|
|
func TestPrefixMismatchCheck_Fix(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
mayorDir := filepath.Join(tmpDir, "mayor")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(mayorDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create routes.jsonl with gt- prefix
|
|
routesPath := filepath.Join(beadsDir, "routes.jsonl")
|
|
routesContent := `{"prefix":"gt-","path":"gastown/mayor/rig"}`
|
|
if err := os.WriteFile(routesPath, []byte(routesContent), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create rigs.json with WRONG prefix (ga instead of gt)
|
|
rigsPath := filepath.Join(mayorDir, "rigs.json")
|
|
rigsContent := `{
|
|
"version": 1,
|
|
"rigs": {
|
|
"gastown": {
|
|
"git_url": "https://github.com/example/gastown",
|
|
"beads": {
|
|
"prefix": "ga"
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
if err := os.WriteFile(rigsPath, []byte(rigsContent), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
check := NewPrefixMismatchCheck()
|
|
ctx := &CheckContext{TownRoot: tmpDir}
|
|
|
|
// First verify there's a mismatch
|
|
result := check.Run(ctx)
|
|
if result.Status != StatusWarning {
|
|
t.Fatalf("expected mismatch before fix, got %v", result.Status)
|
|
}
|
|
|
|
// Fix it
|
|
if err := check.Fix(ctx); err != nil {
|
|
t.Fatalf("Fix() failed: %v", err)
|
|
}
|
|
|
|
// Verify it's now fixed
|
|
result = check.Run(ctx)
|
|
if result.Status != StatusOK {
|
|
t.Errorf("expected StatusOK after fix, got %v: %s", result.Status, result.Message)
|
|
}
|
|
|
|
// Verify rigs.json was updated
|
|
data, err := os.ReadFile(rigsPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := loadRigsConfig(rigsPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to load fixed rigs.json: %v (content: %s)", err, data)
|
|
}
|
|
if cfg.Rigs["gastown"].BeadsConfig.Prefix != "gt" {
|
|
t.Errorf("expected prefix 'gt' after fix, got %q", cfg.Rigs["gastown"].BeadsConfig.Prefix)
|
|
}
|
|
}
|