* 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>
596 lines
18 KiB
Go
596 lines
18 KiB
Go
//go:build integration
|
|
|
|
// Package cmd contains integration tests for beads routing and redirects.
|
|
//
|
|
// Run with: go test -tags=integration ./internal/cmd -run TestBeadsRouting -v
|
|
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/steveyegge/gastown/internal/beads"
|
|
)
|
|
|
|
// setupRoutingTestTown creates a minimal Gas Town with multiple rigs for testing routing.
|
|
// Returns townRoot.
|
|
func setupRoutingTestTown(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
townRoot := t.TempDir()
|
|
|
|
// Create town-level .beads directory
|
|
townBeadsDir := filepath.Join(townRoot, ".beads")
|
|
if err := os.MkdirAll(townBeadsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir town .beads: %v", err)
|
|
}
|
|
|
|
// Create routes.jsonl with multiple rigs
|
|
routes := []beads.Route{
|
|
{Prefix: "hq-", Path: "."}, // Town-level beads
|
|
{Prefix: "gt-", Path: "gastown/mayor/rig"}, // Gastown rig
|
|
{Prefix: "tr-", Path: "testrig/mayor/rig"}, // Test rig
|
|
}
|
|
if err := beads.WriteRoutes(townBeadsDir, routes); err != nil {
|
|
t.Fatalf("write routes: %v", err)
|
|
}
|
|
|
|
// Create gastown rig structure
|
|
gasRigPath := filepath.Join(townRoot, "gastown", "mayor", "rig")
|
|
if err := os.MkdirAll(gasRigPath, 0755); err != nil {
|
|
t.Fatalf("mkdir gastown: %v", err)
|
|
}
|
|
|
|
// Create gastown .beads directory with its own config
|
|
gasBeadsDir := filepath.Join(gasRigPath, ".beads")
|
|
if err := os.MkdirAll(gasBeadsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir gastown .beads: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(gasBeadsDir, "config.yaml"), []byte("prefix: gt\n"), 0644); err != nil {
|
|
t.Fatalf("write gastown config: %v", err)
|
|
}
|
|
|
|
// Create testrig structure
|
|
testRigPath := filepath.Join(townRoot, "testrig", "mayor", "rig")
|
|
if err := os.MkdirAll(testRigPath, 0755); err != nil {
|
|
t.Fatalf("mkdir testrig: %v", err)
|
|
}
|
|
|
|
// Create testrig .beads directory
|
|
testBeadsDir := filepath.Join(testRigPath, ".beads")
|
|
if err := os.MkdirAll(testBeadsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir testrig .beads: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(testBeadsDir, "config.yaml"), []byte("prefix: tr\n"), 0644); err != nil {
|
|
t.Fatalf("write testrig config: %v", err)
|
|
}
|
|
|
|
// Create polecats directory with redirect
|
|
polecatsDir := filepath.Join(townRoot, "gastown", "polecats", "rictus")
|
|
if err := os.MkdirAll(polecatsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir polecats: %v", err)
|
|
}
|
|
|
|
// Create redirect file for polecat -> mayor/rig/.beads
|
|
// Path: gastown/polecats/rictus -> ../../mayor/rig/.beads -> gastown/mayor/rig/.beads
|
|
polecatBeadsDir := filepath.Join(polecatsDir, ".beads")
|
|
if err := os.MkdirAll(polecatBeadsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir polecat .beads: %v", err)
|
|
}
|
|
redirectContent := "../../mayor/rig/.beads"
|
|
if err := os.WriteFile(filepath.Join(polecatBeadsDir, "redirect"), []byte(redirectContent), 0644); err != nil {
|
|
t.Fatalf("write redirect: %v", err)
|
|
}
|
|
|
|
// Create crew directory with redirect
|
|
// Path: gastown/crew/max -> ../../mayor/rig/.beads -> gastown/mayor/rig/.beads
|
|
crewDir := filepath.Join(townRoot, "gastown", "crew", "max")
|
|
if err := os.MkdirAll(crewDir, 0755); err != nil {
|
|
t.Fatalf("mkdir crew: %v", err)
|
|
}
|
|
|
|
crewBeadsDir := filepath.Join(crewDir, ".beads")
|
|
if err := os.MkdirAll(crewBeadsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir crew .beads: %v", err)
|
|
}
|
|
crewRedirect := "../../mayor/rig/.beads"
|
|
if err := os.WriteFile(filepath.Join(crewBeadsDir, "redirect"), []byte(crewRedirect), 0644); err != nil {
|
|
t.Fatalf("write crew redirect: %v", err)
|
|
}
|
|
|
|
return townRoot
|
|
}
|
|
|
|
func initBeadsDBWithPrefix(t *testing.T, dir, prefix string) {
|
|
t.Helper()
|
|
|
|
cmd := exec.Command("bd", "--no-daemon", "init", "--quiet", "--prefix", prefix)
|
|
cmd.Dir = dir
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("bd init failed in %s: %v\n%s", dir, err, output)
|
|
}
|
|
|
|
// Create empty issues.jsonl to prevent bd auto-export from corrupting routes.jsonl.
|
|
// Without this, bd create writes issue data to routes.jsonl (the first .jsonl file
|
|
// it finds), corrupting the routing configuration. This mirrors what gt install does.
|
|
issuesPath := filepath.Join(dir, ".beads", "issues.jsonl")
|
|
if err := os.WriteFile(issuesPath, []byte(""), 0644); err != nil {
|
|
t.Fatalf("create issues.jsonl in %s: %v", dir, err)
|
|
}
|
|
}
|
|
|
|
func createTestIssue(t *testing.T, dir, title string) *beads.Issue {
|
|
t.Helper()
|
|
|
|
args := []string{"--no-daemon", "create", "--json", "--title", title, "--type", "task",
|
|
"--description", "Integration test issue"}
|
|
cmd := exec.Command("bd", args...)
|
|
cmd.Dir = dir
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
combinedCmd := exec.Command("bd", args...)
|
|
combinedCmd.Dir = dir
|
|
combinedOutput, _ := combinedCmd.CombinedOutput()
|
|
t.Fatalf("create issue in %s: %v\n%s", dir, err, combinedOutput)
|
|
}
|
|
|
|
var issue beads.Issue
|
|
if err := json.Unmarshal(output, &issue); err != nil {
|
|
t.Fatalf("parse create output in %s: %v", dir, err)
|
|
}
|
|
if issue.ID == "" {
|
|
t.Fatalf("create issue in %s returned empty ID", dir)
|
|
}
|
|
return &issue
|
|
}
|
|
|
|
func hasIssueID(issues []*beads.Issue, id string) bool {
|
|
for _, issue := range issues {
|
|
if issue.ID == id {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// TestBeadsRoutingFromTownRoot verifies that bd show routes to correct rig
|
|
// based on issue ID prefix when run from town root.
|
|
func TestBeadsRoutingFromTownRoot(t *testing.T) {
|
|
// Skip if bd is not available
|
|
if _, err := exec.LookPath("bd"); err != nil {
|
|
t.Skip("bd not installed, skipping routing test")
|
|
}
|
|
|
|
townRoot := setupRoutingTestTown(t)
|
|
|
|
initBeadsDBWithPrefix(t, townRoot, "hq")
|
|
|
|
gastownRigPath := filepath.Join(townRoot, "gastown", "mayor", "rig")
|
|
testrigRigPath := filepath.Join(townRoot, "testrig", "mayor", "rig")
|
|
initBeadsDBWithPrefix(t, gastownRigPath, "gt")
|
|
initBeadsDBWithPrefix(t, testrigRigPath, "tr")
|
|
|
|
townIssue := createTestIssue(t, townRoot, "Town-level routing test")
|
|
gastownIssue := createTestIssue(t, gastownRigPath, "Gastown routing test")
|
|
testrigIssue := createTestIssue(t, testrigRigPath, "Testrig routing test")
|
|
|
|
tests := []struct {
|
|
id string
|
|
title string
|
|
}{
|
|
{townIssue.ID, townIssue.Title},
|
|
{gastownIssue.ID, gastownIssue.Title},
|
|
{testrigIssue.ID, testrigIssue.Title},
|
|
}
|
|
|
|
townBeads := beads.New(townRoot)
|
|
for _, tc := range tests {
|
|
t.Run(tc.id, func(t *testing.T) {
|
|
issue, err := townBeads.Show(tc.id)
|
|
if err != nil {
|
|
t.Fatalf("bd show %s failed: %v", tc.id, err)
|
|
}
|
|
if issue.ID != tc.id {
|
|
t.Errorf("issue.ID = %s, want %s", issue.ID, tc.id)
|
|
}
|
|
if issue.Title != tc.title {
|
|
t.Errorf("issue.Title = %q, want %q", issue.Title, tc.title)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBeadsRedirectResolution verifies that redirect files are followed correctly.
|
|
func TestBeadsRedirectResolution(t *testing.T) {
|
|
townRoot := setupRoutingTestTown(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
workDir string
|
|
expected string // Expected resolved path (relative to townRoot)
|
|
}{
|
|
{
|
|
name: "polecat redirect",
|
|
workDir: "gastown/polecats/rictus",
|
|
expected: "gastown/mayor/rig/.beads",
|
|
},
|
|
{
|
|
name: "crew redirect",
|
|
workDir: "gastown/crew/max",
|
|
expected: "gastown/mayor/rig/.beads",
|
|
},
|
|
{
|
|
name: "no redirect (mayor/rig)",
|
|
workDir: "gastown/mayor/rig",
|
|
expected: "gastown/mayor/rig/.beads",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
fullWorkDir := filepath.Join(townRoot, tc.workDir)
|
|
resolved := beads.ResolveBeadsDir(fullWorkDir)
|
|
|
|
expectedFull := filepath.Join(townRoot, tc.expected)
|
|
if resolved != expectedFull {
|
|
t.Errorf("ResolveBeadsDir(%s) = %s, want %s", tc.workDir, resolved, expectedFull)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBeadsCircularRedirectDetection verifies that circular redirects are detected.
|
|
func TestBeadsCircularRedirectDetection(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a beads directory with a redirect pointing to itself
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
|
|
// Create redirect file pointing to itself (circular)
|
|
redirectContent := ".beads" // Points to current .beads (circular)
|
|
if err := os.WriteFile(filepath.Join(beadsDir, "redirect"), []byte(redirectContent), 0644); err != nil {
|
|
t.Fatalf("write redirect: %v", err)
|
|
}
|
|
|
|
// ResolveBeadsDir should detect the circular redirect and return the original
|
|
resolved := beads.ResolveBeadsDir(tmpDir)
|
|
if resolved != beadsDir {
|
|
t.Errorf("expected circular redirect to return original beads dir, got %s", resolved)
|
|
}
|
|
|
|
// The redirect file should have been removed
|
|
redirectPath := filepath.Join(beadsDir, "redirect")
|
|
if _, err := os.Stat(redirectPath); !os.IsNotExist(err) {
|
|
t.Error("circular redirect file should have been removed")
|
|
}
|
|
}
|
|
|
|
// TestBeadsPrefixConflictDetection verifies that duplicate prefixes are detected.
|
|
func TestBeadsPrefixConflictDetection(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
|
|
// Create routes with a duplicate prefix
|
|
routes := []beads.Route{
|
|
{Prefix: "gt-", Path: "gastown/mayor/rig"},
|
|
{Prefix: "gt-", Path: "other/mayor/rig"}, // Duplicate!
|
|
{Prefix: "bd-", Path: "beads/mayor/rig"},
|
|
}
|
|
if err := beads.WriteRoutes(beadsDir, routes); err != nil {
|
|
t.Fatalf("write routes: %v", err)
|
|
}
|
|
|
|
// FindConflictingPrefixes should detect the duplicate
|
|
conflicts, err := beads.FindConflictingPrefixes(beadsDir)
|
|
if err != nil {
|
|
t.Fatalf("FindConflictingPrefixes: %v", err)
|
|
}
|
|
|
|
if len(conflicts) == 0 {
|
|
t.Error("expected to find conflicts, got none")
|
|
}
|
|
|
|
if paths, ok := conflicts["gt-"]; !ok {
|
|
t.Error("expected conflict for prefix 'gt-'")
|
|
} else if len(paths) != 2 {
|
|
t.Errorf("expected 2 conflicting paths for 'gt-', got %d", len(paths))
|
|
}
|
|
}
|
|
|
|
// TestBeadsListFromPolecatDirectory verifies that bd list works from polecat directories.
|
|
func TestBeadsListFromPolecatDirectory(t *testing.T) {
|
|
// Skip if bd is not available
|
|
if _, err := exec.LookPath("bd"); err != nil {
|
|
t.Skip("bd not installed, skipping test")
|
|
}
|
|
|
|
townRoot := setupRoutingTestTown(t)
|
|
polecatDir := filepath.Join(townRoot, "gastown", "polecats", "rictus")
|
|
|
|
rigPath := filepath.Join(townRoot, "gastown", "mayor", "rig")
|
|
initBeadsDBWithPrefix(t, rigPath, "gt")
|
|
|
|
issue := createTestIssue(t, rigPath, "Polecat list redirect test")
|
|
|
|
issues, err := beads.New(polecatDir).List(beads.ListOptions{
|
|
Status: "open",
|
|
Priority: -1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("bd list from polecat dir failed: %v", err)
|
|
}
|
|
|
|
if !hasIssueID(issues, issue.ID) {
|
|
t.Errorf("bd list from polecat dir missing issue %s", issue.ID)
|
|
}
|
|
}
|
|
|
|
// TestBeadsListFromCrewDirectory verifies that bd list works from crew directories.
|
|
func TestBeadsListFromCrewDirectory(t *testing.T) {
|
|
// Skip if bd is not available
|
|
if _, err := exec.LookPath("bd"); err != nil {
|
|
t.Skip("bd not installed, skipping test")
|
|
}
|
|
|
|
townRoot := setupRoutingTestTown(t)
|
|
crewDir := filepath.Join(townRoot, "gastown", "crew", "max")
|
|
|
|
rigPath := filepath.Join(townRoot, "gastown", "mayor", "rig")
|
|
initBeadsDBWithPrefix(t, rigPath, "gt")
|
|
|
|
issue := createTestIssue(t, rigPath, "Crew list redirect test")
|
|
|
|
issues, err := beads.New(crewDir).List(beads.ListOptions{
|
|
Status: "open",
|
|
Priority: -1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("bd list from crew dir failed: %v", err)
|
|
}
|
|
if !hasIssueID(issues, issue.ID) {
|
|
t.Errorf("bd list from crew dir missing issue %s", issue.ID)
|
|
}
|
|
}
|
|
|
|
// TestBeadsRoutesLoading verifies that routes.jsonl is loaded correctly.
|
|
func TestBeadsRoutesLoading(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
|
|
// Create routes.jsonl with various entries
|
|
routesContent := `{"prefix": "hq-", "path": "."}
|
|
{"prefix": "gt-", "path": "gastown/mayor/rig"}
|
|
# Comment line should be ignored
|
|
{"prefix": "bd-", "path": "beads/mayor/rig"}
|
|
|
|
{"prefix": "tr-", "path": "testrig/mayor/rig"}
|
|
`
|
|
if err := os.WriteFile(filepath.Join(beadsDir, "routes.jsonl"), []byte(routesContent), 0644); err != nil {
|
|
t.Fatalf("write routes: %v", err)
|
|
}
|
|
|
|
routes, err := beads.LoadRoutes(beadsDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadRoutes: %v", err)
|
|
}
|
|
|
|
if len(routes) != 4 {
|
|
t.Errorf("expected 4 routes, got %d", len(routes))
|
|
}
|
|
|
|
// Verify specific routes
|
|
expectedPrefixes := map[string]string{
|
|
"hq-": ".",
|
|
"gt-": "gastown/mayor/rig",
|
|
"bd-": "beads/mayor/rig",
|
|
"tr-": "testrig/mayor/rig",
|
|
}
|
|
|
|
for _, r := range routes {
|
|
if expected, ok := expectedPrefixes[r.Prefix]; ok {
|
|
if r.Path != expected {
|
|
t.Errorf("route %s: path = %q, want %q", r.Prefix, r.Path, expected)
|
|
}
|
|
} else {
|
|
t.Errorf("unexpected prefix: %s", r.Prefix)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBeadsAppendRoute verifies that routes can be appended and updated.
|
|
func TestBeadsAppendRoute(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
|
|
// Append first route
|
|
route1 := beads.Route{Prefix: "gt-", Path: "gastown/mayor/rig"}
|
|
if err := beads.AppendRoute(tmpDir, route1); err != nil {
|
|
t.Fatalf("AppendRoute 1: %v", err)
|
|
}
|
|
|
|
// Append second route
|
|
route2 := beads.Route{Prefix: "bd-", Path: "beads/mayor/rig"}
|
|
if err := beads.AppendRoute(tmpDir, route2); err != nil {
|
|
t.Fatalf("AppendRoute 2: %v", err)
|
|
}
|
|
|
|
// Verify both routes exist
|
|
routes, err := beads.LoadRoutes(beadsDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadRoutes: %v", err)
|
|
}
|
|
if len(routes) != 2 {
|
|
t.Errorf("expected 2 routes, got %d", len(routes))
|
|
}
|
|
|
|
// Update existing route (same prefix, different path)
|
|
route1Updated := beads.Route{Prefix: "gt-", Path: "newpath/mayor/rig"}
|
|
if err := beads.AppendRoute(tmpDir, route1Updated); err != nil {
|
|
t.Fatalf("AppendRoute update: %v", err)
|
|
}
|
|
|
|
// Verify update
|
|
routes, _ = beads.LoadRoutes(beadsDir)
|
|
if len(routes) != 2 {
|
|
t.Errorf("expected 2 routes after update, got %d", len(routes))
|
|
}
|
|
|
|
for _, r := range routes {
|
|
if r.Prefix == "gt-" && r.Path != "newpath/mayor/rig" {
|
|
t.Errorf("route update failed: got path %q", r.Path)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBeadsRemoveRoute verifies that routes can be removed.
|
|
func TestBeadsRemoveRoute(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
|
|
// Create initial routes
|
|
routes := []beads.Route{
|
|
{Prefix: "gt-", Path: "gastown/mayor/rig"},
|
|
{Prefix: "bd-", Path: "beads/mayor/rig"},
|
|
}
|
|
if err := beads.WriteRoutes(beadsDir, routes); err != nil {
|
|
t.Fatalf("WriteRoutes: %v", err)
|
|
}
|
|
|
|
// Remove one route
|
|
if err := beads.RemoveRoute(tmpDir, "gt-"); err != nil {
|
|
t.Fatalf("RemoveRoute: %v", err)
|
|
}
|
|
|
|
// Verify removal
|
|
remaining, _ := beads.LoadRoutes(beadsDir)
|
|
if len(remaining) != 1 {
|
|
t.Errorf("expected 1 route after removal, got %d", len(remaining))
|
|
}
|
|
if remaining[0].Prefix != "bd-" {
|
|
t.Errorf("wrong route remaining: %s", remaining[0].Prefix)
|
|
}
|
|
}
|
|
|
|
// TestSlingCrossRigRoutingResolution verifies that sling can resolve rig paths
|
|
// for cross-rig bead hooking using ExtractPrefix and GetRigPathForPrefix.
|
|
// This is the fix for https://github.com/steveyegge/gastown/issues/148
|
|
func TestSlingCrossRigRoutingResolution(t *testing.T) {
|
|
townRoot := setupRoutingTestTown(t)
|
|
|
|
tests := []struct {
|
|
beadID string
|
|
expectedPath string // Relative to townRoot, or "." for town-level
|
|
}{
|
|
{"gt-mol-abc", "gastown/mayor/rig"},
|
|
{"tr-task-xyz", "testrig/mayor/rig"},
|
|
{"hq-cv-123", "."}, // Town-level beads
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.beadID, func(t *testing.T) {
|
|
// Step 1: Extract prefix from bead ID
|
|
prefix := beads.ExtractPrefix(tc.beadID)
|
|
if prefix == "" {
|
|
t.Fatalf("ExtractPrefix(%q) returned empty", tc.beadID)
|
|
}
|
|
|
|
// Step 2: Resolve rig path from prefix
|
|
rigPath := beads.GetRigPathForPrefix(townRoot, prefix)
|
|
if rigPath == "" {
|
|
t.Fatalf("GetRigPathForPrefix(%q, %q) returned empty", townRoot, prefix)
|
|
}
|
|
|
|
// Step 3: Verify the path is correct
|
|
var expectedFull string
|
|
if tc.expectedPath == "." {
|
|
expectedFull = townRoot
|
|
} else {
|
|
expectedFull = filepath.Join(townRoot, tc.expectedPath)
|
|
}
|
|
|
|
if rigPath != expectedFull {
|
|
t.Errorf("GetRigPathForPrefix resolved to %q, want %q", rigPath, expectedFull)
|
|
}
|
|
|
|
// Step 4: Verify the .beads directory exists at that path
|
|
beadsDir := filepath.Join(rigPath, ".beads")
|
|
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
|
t.Errorf(".beads directory doesn't exist at resolved path: %s", beadsDir)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSlingCrossRigUnknownPrefix verifies behavior for unknown prefixes.
|
|
func TestSlingCrossRigUnknownPrefix(t *testing.T) {
|
|
townRoot := setupRoutingTestTown(t)
|
|
|
|
// An unknown prefix should return empty string
|
|
unknownBeadID := "xx-unknown-123"
|
|
prefix := beads.ExtractPrefix(unknownBeadID)
|
|
if prefix != "xx-" {
|
|
t.Fatalf("ExtractPrefix(%q) = %q, want %q", unknownBeadID, prefix, "xx-")
|
|
}
|
|
|
|
rigPath := beads.GetRigPathForPrefix(townRoot, prefix)
|
|
if rigPath != "" {
|
|
t.Errorf("GetRigPathForPrefix for unknown prefix returned %q, want empty", rigPath)
|
|
}
|
|
}
|
|
|
|
// TestBeadsGetPrefixForRig verifies prefix lookup by rig name.
|
|
func TestBeadsGetPrefixForRig(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
|
|
// Create routes
|
|
routes := []beads.Route{
|
|
{Prefix: "gt-", Path: "gastown/mayor/rig"},
|
|
{Prefix: "bd-", Path: "beads/mayor/rig"},
|
|
{Prefix: "hq-", Path: "."},
|
|
}
|
|
if err := beads.WriteRoutes(beadsDir, routes); err != nil {
|
|
t.Fatalf("WriteRoutes: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
rigName string
|
|
expected string
|
|
}{
|
|
{"gastown", "gt"},
|
|
{"beads", "bd"},
|
|
{"unknown", "gt"}, // Default
|
|
{"", "gt"}, // Empty -> default
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.rigName, func(t *testing.T) {
|
|
result := beads.GetPrefixForRig(tmpDir, tc.rigName)
|
|
if result != tc.expected {
|
|
t.Errorf("GetPrefixForRig(%q) = %q, want %q", tc.rigName, result, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|