Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions internal/cmd/hooks_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,16 @@ func TestRunHooksSyncNonClaudeAgent(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("HOME", tmpDir)

// Put a dummy opencode binary on PATH so agent resolution doesn't fall back to claude.
binDir := filepath.Join(tmpDir, "bin")
if err := os.MkdirAll(binDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(binDir, "opencode"), []byte("#!/bin/sh\n"), 0755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))

townRoot := filepath.Join(tmpDir, "town")

// Scaffold workspace: mayor, deacon, and a rig with a crew worktree
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/patrol_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ func TestBuildRefineryPatrolVars_FullConfig(t *testing.T) {
"run_tests": "true",
"target_branch": "main",
"delete_merged_branches": "true",
"judgment_enabled": "false",
"review_depth": "standard",
}

varMap := make(map[string]string)
Expand Down
17 changes: 17 additions & 0 deletions internal/doctor/hooks_sync_check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ func scaffoldWorkspace(t *testing.T, roleAgents map[string]string) string {
tmpDir := t.TempDir()
t.Setenv("HOME", tmpDir)

// Put dummy binaries for non-Claude role agents on PATH so agent
// resolution doesn't fall back to claude when the binary is missing.
if len(roleAgents) > 0 {
binDir := filepath.Join(tmpDir, "bin")
if err := os.MkdirAll(binDir, 0755); err != nil {
t.Fatal(err)
}
for _, agent := range roleAgents {
if agent != "" && agent != "claude" {
if err := os.WriteFile(filepath.Join(binDir, agent), []byte("#!/bin/sh\n"), 0755); err != nil {
t.Fatal(err)
}
}
}
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
}

townRoot := filepath.Join(tmpDir, "town")

// Required workspace structure
Expand Down
20 changes: 20 additions & 0 deletions internal/doltserver/doltserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2977,11 +2977,31 @@ func EnsureAllMetadata(townRoot string) (updated []string, errs []error) {
dbToRig[k] = v
}

// Build reverse map: rig → canonical DB name. When an orphaned database
// shares a rig's directory name (e.g., "gastown" db alongside canonical "gt"),
// both would write to the same metadata.json with different dolt_database
// values, causing flip-flop warnings on every startup.
rigToCanonicalDB := make(map[string]string)
for dbName, rigName := range dbToRig {
rigToCanonicalDB[rigName] = dbName
}

for _, dbName := range databases {
rigName := dbName
if mapped, ok := dbToRig[dbName]; ok {
rigName = mapped
}

// Skip orphan databases that share a rig's directory name but aren't
// that rig's canonical database. E.g., skip "gastown" db when rig
// "gastown" uses "gt" as its canonical DB — processing both would
// flip-flop the metadata.json dolt_database value on every startup.
if _, inMap := dbToRig[dbName]; !inMap {
if canonicalDB, hasCanonical := rigToCanonicalDB[rigName]; hasCanonical && canonicalDB != dbName {
continue
}
}

// Special case: "hq" database maps to "hq" rig (town-level)
if dbName == "hq" {
rigName = "hq"
Expand Down
75 changes: 75 additions & 0 deletions internal/doltserver/doltserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4164,6 +4164,81 @@ func TestEnsureAllMetadata_FallbackToDbName(t *testing.T) {
}
}

// TestEnsureAllMetadata_SkipsOrphanDatabases verifies that orphan databases
// sharing a rig's directory name don't flip-flop the metadata.json dolt_database
// value. E.g., if rig "gastown" uses canonical DB "gt" and an orphan "gastown"
// DB also exists, only "gt" should write to gastown's metadata.json.
func TestEnsureAllMetadata_SkipsOrphanDatabases(t *testing.T) {
townRoot := t.TempDir()

// Create both canonical and orphan databases
dataDir := filepath.Join(townRoot, ".dolt-data")
setupDoltDB(t, dataDir, "hq")
setupDoltDB(t, dataDir, "gt") // canonical DB for gastown rig
setupDoltDB(t, dataDir, "gastown") // orphan with same name as rig
setupDoltDB(t, dataDir, "mo") // canonical DB for monorepo rig
setupDoltDB(t, dataDir, "monorepo") // orphan with same name as rig

// Create routes.jsonl mapping prefixes to rigs
beadsDir := filepath.Join(townRoot, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatal(err)
}
routesContent := `{"prefix":"hq-","path":"."}
{"prefix":"gt-","path":"gastown/mayor/rig"}
{"prefix":"mo-","path":"monorepo/mayor/rig"}
`
if err := os.WriteFile(filepath.Join(beadsDir, "routes.jsonl"), []byte(routesContent), 0644); err != nil {
t.Fatal(err)
}

// Create rig beads directories
if err := os.MkdirAll(filepath.Join(townRoot, "gastown", "mayor", "rig", ".beads"), 0755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(townRoot, "monorepo", "mayor", "rig", ".beads"), 0755); err != nil {
t.Fatal(err)
}

// Run EnsureAllMetadata
updated, errs := EnsureAllMetadata(townRoot)
if len(errs) > 0 {
t.Errorf("unexpected errors: %v", errs)
}

// Should only process canonical DBs (hq, gt, mo), NOT orphans (gastown, monorepo)
if len(updated) != 3 {
t.Errorf("expected 3 updated databases (hq, gt, mo), got %d: %v", len(updated), updated)
}

// Verify gastown metadata has "gt" (canonical), not "gastown" (orphan)
gastownMeta := filepath.Join(townRoot, "gastown", "mayor", "rig", ".beads", "metadata.json")
data, err := os.ReadFile(gastownMeta)
if err != nil {
t.Fatalf("reading gastown metadata: %v", err)
}
var meta map[string]interface{}
if err := json.Unmarshal(data, &meta); err != nil {
t.Fatalf("parsing gastown metadata: %v", err)
}
if meta["dolt_database"] != "gt" {
t.Errorf("gastown dolt_database should be %q (canonical), got %q", "gt", meta["dolt_database"])
}

// Verify monorepo metadata has "mo" (canonical), not "monorepo" (orphan)
monorepoMeta := filepath.Join(townRoot, "monorepo", "mayor", "rig", ".beads", "metadata.json")
data, err = os.ReadFile(monorepoMeta)
if err != nil {
t.Fatalf("reading monorepo metadata: %v", err)
}
if err := json.Unmarshal(data, &meta); err != nil {
t.Fatalf("parsing monorepo metadata: %v", err)
}
if meta["dolt_database"] != "mo" {
t.Errorf("monorepo dolt_database should be %q (canonical), got %q", "mo", meta["dolt_database"])
}
}

func TestCleanStaleSocket_RemovesStaleFile(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Unix sockets not applicable on Windows")
Expand Down
2 changes: 2 additions & 0 deletions internal/hooks/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ func resolveAndSubstitute(provider, hooksFile, role string) ([]byte, error) {
}

// writeTemplate resolves a template, substitutes placeholders, and writes it to targetPath.
//
//nolint:unparam // hooksDir kept for API symmetry with InstallForRole
func writeTemplate(provider, role, hooksDir, hooksFile, targetPath string) error {
content, err := resolveAndSubstitute(provider, hooksFile, role)
if err != nil {
Expand Down
Loading