fix(sync): use town-level routes for prefix validation

Follow-up to 828fc11b addressing code review feedback:

1. Added LoadTownRoutes() - exported function that walks up to find
   town-level routes.jsonl (e.g., ~/gt/.beads/routes.jsonl)

2. Updated buildAllowedPrefixSet to use LoadTownRoutes instead of
   LoadRoutes, so it finds routes even when importing from a rig's
   local beads directory

3. Added unit tests for buildAllowedPrefixSet covering:
   - Primary prefix inclusion
   - allowed_prefixes config parsing
   - Routes from routes.jsonl
   - Missing routes.jsonl handling
   - Empty beadsDir handling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
dennis
2026-01-17 09:51:36 -08:00
committed by Steve Yegge
parent 4f0f5744a6
commit 1f59119144
3 changed files with 108 additions and 8 deletions

View File

@@ -1058,15 +1058,14 @@ func buildAllowedPrefixSet(primaryPrefix string, allowedPrefixesConfig string, b
// Load prefixes from routes.jsonl for multi-rig setups (Gas Town)
// This allows issues from other rigs to coexist in the same JSONL
// Use LoadTownRoutes to find routes at town level (~/gt/.beads/routes.jsonl)
if beadsDir != "" {
routes, err := routing.LoadRoutes(beadsDir)
if err == nil && len(routes) > 0 {
for _, route := range routes {
// Normalize: remove trailing - if present
prefix := strings.TrimSuffix(route.Prefix, "-")
if prefix != "" {
allowed[prefix] = true
}
routes, _ := routing.LoadTownRoutes(beadsDir)
for _, route := range routes {
// Normalize: remove trailing - if present
prefix := strings.TrimSuffix(route.Prefix, "-")
if prefix != "" {
allowed[prefix] = true
}
}
}

View File

@@ -5,6 +5,8 @@ package importer
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -1708,3 +1710,92 @@ func TestMultiRepoPrefixValidation(t *testing.T) {
}
})
}
func TestBuildAllowedPrefixSet(t *testing.T) {
t.Run("includes primary prefix", func(t *testing.T) {
allowed := buildAllowedPrefixSet("gt", "", "")
if allowed == nil {
t.Fatal("Expected non-nil allowed set")
}
if !allowed["gt"] {
t.Error("Primary prefix 'gt' should be allowed")
}
})
t.Run("includes allowed_prefixes config", func(t *testing.T) {
allowed := buildAllowedPrefixSet("gt", "hq,mol-,other", "")
if allowed == nil {
t.Fatal("Expected non-nil allowed set")
}
if !allowed["gt"] {
t.Error("Primary prefix 'gt' should be allowed")
}
if !allowed["hq"] {
t.Error("Config prefix 'hq' should be allowed")
}
if !allowed["mol"] {
t.Error("Config prefix 'mol' (normalized from 'mol-') should be allowed")
}
if !allowed["other"] {
t.Error("Config prefix 'other' should be allowed")
}
})
t.Run("includes prefixes from routes.jsonl", func(t *testing.T) {
// Create a temp directory with routes.jsonl
tmpDir := t.TempDir()
routesPath := filepath.Join(tmpDir, "routes.jsonl")
routesContent := `{"prefix": "hq-", "path": "."}
{"prefix": "gt-", "path": "gastown/mayor/rig"}
{"prefix": "bd-", "path": "beads/mayor/rig"}
`
if err := os.WriteFile(routesPath, []byte(routesContent), 0644); err != nil {
t.Fatalf("Failed to write routes.jsonl: %v", err)
}
// buildAllowedPrefixSet uses LoadTownRoutes which tries to find town root
// For this unit test, LoadRoutes will work since routes.jsonl is in tmpDir
allowed := buildAllowedPrefixSet("gt", "", tmpDir)
if allowed == nil {
t.Fatal("Expected non-nil allowed set")
}
// Primary prefix should always be included
if !allowed["gt"] {
t.Error("Primary prefix 'gt' should be allowed")
}
// Routed prefixes should be included (normalized without trailing -)
if !allowed["hq"] {
t.Error("Routed prefix 'hq' (from routes.jsonl) should be allowed")
}
if !allowed["bd"] {
t.Error("Routed prefix 'bd' (from routes.jsonl) should be allowed")
}
})
t.Run("handles missing routes.jsonl gracefully", func(t *testing.T) {
tmpDir := t.TempDir()
// No routes.jsonl file in tmpDir
// Note: LoadTownRoutes may still find routes at town level if running in Gas Town
allowed := buildAllowedPrefixSet("gt", "", tmpDir)
if allowed == nil {
t.Fatal("Expected non-nil allowed set")
}
if !allowed["gt"] {
t.Error("Primary prefix 'gt' should be allowed even without local routes.jsonl")
}
// Don't check exact count - town-level routes may be found
})
t.Run("handles empty beadsDir", func(t *testing.T) {
allowed := buildAllowedPrefixSet("gt", "", "")
if allowed == nil {
t.Fatal("Expected non-nil allowed set")
}
if !allowed["gt"] {
t.Error("Primary prefix 'gt' should be allowed")
}
})
}

View File

@@ -55,6 +55,16 @@ func LoadRoutes(beadsDir string) ([]Route, error) {
return routes, scanner.Err()
}
// LoadTownRoutes loads routes from the town-level routes.jsonl.
// It first checks the given beadsDir, then walks up to find the town root
// and loads routes from there. This is useful for multi-rig setups (Gas Town)
// where routes.jsonl lives at ~/gt/.beads/ rather than in individual rig directories.
// Returns routes and nil error on success, or nil routes if not in a town or no routes found.
func LoadTownRoutes(beadsDir string) ([]Route, error) {
routes, _ := findTownRoutes(beadsDir)
return routes, nil
}
// ExtractPrefix extracts the prefix from an issue ID.
// For "gt-abc123", returns "gt-".
// For "bd-abc123", returns "bd-".