Skip to content
Open
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
156 changes: 155 additions & 1 deletion cmd/bd/explicit_db_nodb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package main

import (
"context"
"encoding/json"
"os"
"os/exec"
Expand All @@ -11,6 +12,7 @@ import (
"strconv"
"strings"
"testing"
"time"

"github.com/steveyegge/beads/internal/configfile"
)
Expand Down Expand Up @@ -113,6 +115,11 @@ func writeProjectConfig(t *testing.T, beadsDir string, syncRemote string, port i
))
}

func writeIssuePrefixConfig(t *testing.T, beadsDir, prefix string) {
t.Helper()
writeFile(t, filepath.Join(beadsDir, "config.yaml"), []byte("issue-prefix: "+prefix+"\n"))
}

// evalPath resolves symlinks in a path for consistent comparison.
// On macOS, t.TempDir() returns /var/folders/... but binaries resolve
// it to /private/var/folders/..., causing string comparison failures.
Expand All @@ -139,7 +146,10 @@ func decodeJSONOutput(t *testing.T, out []byte, target any) {

func runBDCommand(t *testing.T, binPath, dir string, extraEnv []string, args ...string) []byte {
t.Helper()
cmd := exec.Command(binPath, args...)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

cmd := exec.CommandContext(ctx, binPath, args...)
cmd.Dir = dir
cmd.Env = append(os.Environ(),
"HOME="+t.TempDir(),
Expand All @@ -155,11 +165,44 @@ func runBDCommand(t *testing.T, binPath, dir string, extraEnv []string, args ...
cmd.Env = append(cmd.Env, extraEnv...)
out, err := cmd.CombinedOutput()
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
t.Fatalf("%v timed out after %s\n%s", args, 10*time.Second, out)
}
t.Fatalf("%v failed: %v\n%s", args, err, out)
}
return out
}

type whereExpectation struct {
beadsDir string
database string
prefix string
omitPrefix bool
}

func assertWhereOutput(t *testing.T, out []byte, want whereExpectation) {
t.Helper()

var got map[string]any
decodeJSONOutput(t, out, &got)

if evalPath(t, got["path"].(string)) != evalPath(t, want.beadsDir) {
t.Fatalf("path = %v, want %s", got["path"], want.beadsDir)
}
if evalPath(t, got["database_path"].(string)) != evalPath(t, want.database) {
t.Fatalf("database_path = %v, want %s", got["database_path"], want.database)
}
if want.omitPrefix {
if _, ok := got["prefix"]; ok {
t.Fatalf("prefix = %v, want omitted when only server metadata is available", got["prefix"])
}
return
}
if got["prefix"] != want.prefix {
t.Fatalf("prefix = %v, want %s", got["prefix"], want.prefix)
}
}

func TestContextUsesExplicitDBFlagForNoDBCommand(t *testing.T) {
binPath := buildBDUnderTest(t)
root := t.TempDir()
Expand All @@ -186,6 +229,24 @@ func TestContextUsesExplicitDBFlagForNoDBCommand(t *testing.T) {
}
}

func TestWhereUsesExplicitDBFlagForNoDBCommand(t *testing.T) {
binPath := buildBDUnderTest(t)
root := t.TempDir()
repoA := filepath.Join(root, "repo-a")
repoB := filepath.Join(root, "repo-b")
beadsDirA := writeServerRepo(t, repoA, "repo_a_db", "10.0.0.1", "origin-a", 3311)
beadsDirB := writeServerRepo(t, repoB, "repo_b_db", "10.0.0.2", "origin-b", 3312)
writeIssuePrefixConfig(t, beadsDirA, "repo-a")
writeIssuePrefixConfig(t, beadsDirB, "repo-b")

out := runBDCommand(t, binPath, repoA, nil, "--db", filepath.Join(beadsDirB, "dolt"), "where", "--json")
assertWhereOutput(t, out, whereExpectation{
beadsDir: beadsDirB,
database: filepath.Join(beadsDirB, "dolt"),
prefix: "repo-b",
})
}

func TestDoltShowUsesExplicitDBFlagForNoDBCommand(t *testing.T) {
binPath := buildBDUnderTest(t)
root := t.TempDir()
Expand Down Expand Up @@ -229,6 +290,23 @@ func TestContextUsesBEADSDBForNoDBCommand(t *testing.T) {
}
}

func TestWhereUsesBEADSDBForNoDBCommand(t *testing.T) {
binPath := buildBDUnderTest(t)
root := t.TempDir()
repoA := filepath.Join(root, "repo-a")
repoB := filepath.Join(root, "repo-b")
writeServerRepo(t, repoA, "repo_a_db", "10.0.0.1", "origin-a", 3311)
beadsDirB := writeServerRepo(t, repoB, "repo_b_db", "10.0.0.2", "origin-b", 3312)
writeIssuePrefixConfig(t, beadsDirB, "repo-b")

out := runBDCommand(t, binPath, repoA, []string{"BEADS_DB=" + filepath.Join(beadsDirB, "dolt")}, "where", "--json")
assertWhereOutput(t, out, whereExpectation{
beadsDir: beadsDirB,
database: filepath.Join(beadsDirB, "dolt"),
prefix: "repo-b",
})
}

func TestContextUsesBEADSDBDirectoryForNoDBCommand(t *testing.T) {
binPath := buildBDUnderTest(t)
root := t.TempDir()
Expand Down Expand Up @@ -272,6 +350,23 @@ func TestContextUsesExplicitDBFlagForExternalDoltDataDir(t *testing.T) {
}
}

func TestWhereUsesExplicitDBFlagForExternalDoltDataDir(t *testing.T) {
binPath := buildBDUnderTest(t)
root := t.TempDir()
repoA := filepath.Join(root, "repo-a")
repoB := filepath.Join(root, "repo-b")
writeServerRepo(t, repoA, "repo_a_db", "10.0.0.1", "origin-a", 3311)
beadsDirB := writeServerRepoWithDataDir(t, repoB, "repo_b_db", "10.0.0.2", "origin-b", 3312, "../external-dolt")
writeIssuePrefixConfig(t, beadsDirB, "repo-b")

out := runBDCommand(t, binPath, repoA, nil, "--db", filepath.Join(beadsDirB, "../external-dolt"), "where", "--json")
assertWhereOutput(t, out, whereExpectation{
beadsDir: beadsDirB,
database: filepath.Join(beadsDirB, "../external-dolt"),
prefix: "repo-b",
})
}

func TestContextExplicitDBFlagOverridesBEADSDBForNoDBCommand(t *testing.T) {
binPath := buildBDUnderTest(t)
root := t.TempDir()
Expand All @@ -294,6 +389,65 @@ func TestContextExplicitDBFlagOverridesBEADSDBForNoDBCommand(t *testing.T) {
}
}

func TestWhereExplicitDBFlagOverridesBEADSDBForNoDBCommand(t *testing.T) {
binPath := buildBDUnderTest(t)
root := t.TempDir()
repoA := filepath.Join(root, "repo-a")
repoB := filepath.Join(root, "repo-b")
repoC := filepath.Join(root, "repo-c")
writeServerRepo(t, repoA, "repo_a_db", "10.0.0.1", "origin-a", 3311)
beadsDirB := writeServerRepo(t, repoB, "repo_b_db", "10.0.0.2", "origin-b", 3312)
beadsDirC := writeServerRepo(t, repoC, "repo_c_db", "10.0.0.3", "origin-c", 3313)
writeIssuePrefixConfig(t, beadsDirB, "repo-b")
writeIssuePrefixConfig(t, beadsDirC, "repo-c")

out := runBDCommand(t, binPath, repoA, []string{"BEADS_DB=" + filepath.Join(beadsDirC, "dolt")}, "--db", filepath.Join(beadsDirB, "dolt"), "where", "--json")
assertWhereOutput(t, out, whereExpectation{
beadsDir: beadsDirB,
database: filepath.Join(beadsDirB, "dolt"),
prefix: "repo-b",
})
}

func TestWhereUsesExplicitDBFlagForMetadataOnlyServerRepo(t *testing.T) {
binPath := buildBDUnderTest(t)
root := t.TempDir()
repoA := filepath.Join(root, "repo-a")
repoB := filepath.Join(root, "repo-b")
writeServerRepo(t, repoA, "repo_a_db", "10.0.0.1", "origin-a", 3311)
beadsDirB := writeServerRepo(t, repoB, "repo_b_db", "10.0.0.2", "origin-b", 3312)

out := runBDCommand(t, binPath, repoA, nil, "--db", filepath.Join(beadsDirB, "dolt"), "where", "--json")
assertWhereOutput(t, out, whereExpectation{
beadsDir: beadsDirB,
database: filepath.Join(beadsDirB, "dolt"),
omitPrefix: true,
})
}

func TestWhereBEADSDBOverridesBDDBForNoDBCommand(t *testing.T) {
binPath := buildBDUnderTest(t)
root := t.TempDir()
repoA := filepath.Join(root, "repo-a")
repoB := filepath.Join(root, "repo-b")
repoC := filepath.Join(root, "repo-c")
writeServerRepo(t, repoA, "repo_a_db", "10.0.0.1", "origin-a", 3311)
beadsDirB := writeServerRepo(t, repoB, "repo_b_db", "10.0.0.2", "origin-b", 3312)
beadsDirC := writeServerRepo(t, repoC, "repo_c_db", "10.0.0.3", "origin-c", 3313)
writeIssuePrefixConfig(t, beadsDirB, "repo-b")
writeIssuePrefixConfig(t, beadsDirC, "repo-c")

out := runBDCommand(t, binPath, repoA, []string{
"BEADS_DB=" + filepath.Join(beadsDirB, "dolt"),
"BD_DB=" + filepath.Join(beadsDirC, "dolt"),
}, "where", "--json")
assertWhereOutput(t, out, whereExpectation{
beadsDir: beadsDirB,
database: filepath.Join(beadsDirB, "dolt"),
prefix: "repo-b",
})
}

func TestContextBEADSDBOverridesBDDBForNoDBCommand(t *testing.T) {
binPath := buildBDUnderTest(t)
root := t.TempDir()
Expand Down
12 changes: 6 additions & 6 deletions cmd/bd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,13 +449,13 @@ Non-interactive mode (--non-interactive or BD_NON_INTERACTIVE=1):
if existingCfg, _ := configfile.Load(beadsDir); existingCfg != nil && existingCfg.DoltDatabase != "" {
dbName = existingCfg.DoltDatabase
} else if prefix != "" {
// Sanitize hyphens and dots to underscores for SQL-idiomatic database names.
// Dots are invalid in Dolt/MySQL identifiers (e.g. from ".claude" directories).
// Sanitize hyphens to underscores for SQL-idiomatic database names.
// Dots were already normalized in prefix above so issue_prefix and
// DoltDatabase stay in lockstep.
// Must match the sanitization applied to metadata.json DoltDatabase
// field (line below), otherwise init creates a database with one name
// but metadata.json records a different name, causing reopens to fail.
dbName = strings.ReplaceAll(prefix, "-", "_")
dbName = strings.ReplaceAll(dbName, ".", "_")
} else {
dbName = "beads"
}
Expand Down Expand Up @@ -715,12 +715,12 @@ Non-interactive mode (--non-interactive or BD_NON_INTERACTIVE=1):
if database != "" {
cfg.DoltDatabase = database
} else if cfg.DoltDatabase == "" && prefix != "" {
// Sanitize hyphens and dots to underscores for SQL-idiomatic names (GH#2142).
// Must match the sanitization applied to dbName above (lines 430-431),
// Sanitize hyphens to underscores for SQL-idiomatic names (GH#2142).
// Dots were already normalized in prefix above.
// Must match the sanitization applied to dbName above,
// otherwise init creates a database with one name but metadata.json
// records a different name, causing reopens to fail.
cfg.DoltDatabase = strings.ReplaceAll(prefix, "-", "_")
cfg.DoltDatabase = strings.ReplaceAll(cfg.DoltDatabase, ".", "_")
}

// Persist the connection mode matching this build.
Expand Down
26 changes: 26 additions & 0 deletions cmd/bd/init_embedded_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,32 @@ func TestEmbeddedInit(t *testing.T) {
t.Errorf("issue_prefix: got %q, want %q", val, "GPUPolynomials_jl")
}
})

t.Run("config_dot_prefix_sanitized", func(t *testing.T) {
dir := t.TempDir()
initGitRepoAt(t, dir)
beadsDir := filepath.Join(dir, ".beads")
if err := os.MkdirAll(beadsDir, 0o750); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(beadsDir, "config.yaml"), []byte("issue-prefix: GPUPolynomials.jl\n"), 0o644); err != nil {
t.Fatal(err)
}

runBDInit(t, bd, dir)

cfg, err := configfile.Load(beadsDir)
if err != nil {
t.Fatalf("failed to load metadata.json: %v", err)
}
const want = "GPUPolynomials_jl"
if cfg.DoltDatabase != want {
t.Errorf("DoltDatabase: got %q, want %q", cfg.DoltDatabase, want)
}
if val := readBack(t, beadsDir, want, "issue_prefix", false); val != want {
t.Errorf("issue_prefix: got %q, want %q", val, want)
}
})
}

// TestEmbeddedInitConcurrent verifies the exclusive flock prevents concurrent
Expand Down
2 changes: 1 addition & 1 deletion cmd/bd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func isSelectedNoDBCommand(cmd *cobra.Command) bool {
if cmd == nil {
return false
}
if cmd.Name() == "context" {
if cmd.Name() == "context" || cmd.Name() == "where" {
return true
}
if cmd.Parent() == nil || cmd.Parent().Name() != "dolt" {
Expand Down
Loading
Loading