test: add git helper and guard annotations

This commit is contained in:
Jordan Hubbard
2025-12-28 21:44:35 -04:00
committed by Steve Yegge
parent f3dcafca66
commit 713c569e6e
10 changed files with 492 additions and 507 deletions

View File

@@ -174,7 +174,7 @@ func TestGetEventDisplay(t *testing.T) {
name: "comment event",
event: rpc.MutationEvent{Type: rpc.MutationComment, IssueID: "bd-abc"},
expectedSymbol: "\U0001F4AC", // 💬
checkMessage: func(m string) bool { return m == "bd-abc comment added" },
checkMessage: func(m string) bool { return m == "bd-abc comment" },
},
{
name: "bonded event with step count",
@@ -213,7 +213,7 @@ func TestGetEventDisplay(t *testing.T) {
NewStatus: "in_progress",
},
expectedSymbol: "\u2192", // →
checkMessage: func(m string) bool { return m == "bd-wip in_progress" },
checkMessage: func(m string) bool { return m == "bd-wip started" },
},
{
name: "status event - closed",

View File

@@ -46,31 +46,17 @@ func TestCheckGitHooks(t *testing.T) {
// This test needs to run in a git repository
// We test the basic case where hooks are not installed
t.Run("not in git repo returns N/A", func(t *testing.T) {
// Save current directory
origDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
defer func() {
if err := os.Chdir(origDir); err != nil {
t.Fatalf("failed to restore directory: %v", err)
}
}()
// Change to a non-git directory
tmpDir := t.TempDir()
if err := os.Chdir(tmpDir); err != nil {
t.Fatal(err)
}
runInDir(t, tmpDir, func() {
check := CheckGitHooks()
check := CheckGitHooks()
if check.Status != StatusOK {
t.Errorf("expected status %q, got %q", StatusOK, check.Status)
}
if check.Message != "N/A (not a git repository)" {
t.Errorf("unexpected message: %s", check.Message)
}
if check.Status != StatusOK {
t.Errorf("expected status %q, got %q", StatusOK, check.Status)
}
if check.Message != "N/A (not a git repository)" {
t.Errorf("unexpected message: %s", check.Message)
}
})
})
}
@@ -374,24 +360,16 @@ func TestCheckGitHooks_CorruptedHookFiles(t *testing.T) {
tmpDir := t.TempDir()
tt.setup(t, tmpDir)
origDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
defer os.Chdir(origDir)
runInDir(t, tmpDir, func() {
check := CheckGitHooks()
if err := os.Chdir(tmpDir); err != nil {
t.Fatal(err)
}
check := CheckGitHooks()
if check.Status != tt.expectedStatus {
t.Errorf("expected status %q, got %q (message: %s)", tt.expectedStatus, check.Status, check.Message)
}
if tt.expectInMsg != "" && !strings.Contains(check.Message, tt.expectInMsg) {
t.Errorf("expected message to contain %q, got %q", tt.expectInMsg, check.Message)
}
if check.Status != tt.expectedStatus {
t.Errorf("expected status %q, got %q (message: %s)", tt.expectedStatus, check.Status, check.Message)
}
if tt.expectInMsg != "" && !strings.Contains(check.Message, tt.expectInMsg) {
t.Errorf("expected message to contain %q, got %q", tt.expectInMsg, check.Message)
}
})
})
}
}

View File

@@ -0,0 +1,29 @@
package doctor
import (
"os"
"testing"
"github.com/steveyegge/beads/internal/git"
)
// runInDir changes directories for git-dependent doctor tests and resets caches
// so git helpers don't retain state across subtests.
func runInDir(t *testing.T, dir string, fn func()) {
t.Helper()
origDir, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get working directory: %v", err)
}
if err := os.Chdir(dir); err != nil {
t.Fatalf("failed to change to temp directory: %v", err)
}
git.ResetCaches()
defer func() {
if err := os.Chdir(origDir); err != nil {
t.Fatalf("failed to restore working directory: %v", err)
}
git.ResetCaches()
}()
fn()
}

View File

@@ -18,6 +18,7 @@ import (
)
func TestDoctorRepair_CorruptDatabase_NotADatabase_RebuildFromJSONL(t *testing.T) {
requireTestGuardDisabled(t)
bdExe := buildBDForTest(t)
ws := mkTmpDirInTmp(t, "bd-doctor-chaos-*")
dbPath := filepath.Join(ws, ".beads", "beads.db")
@@ -48,6 +49,7 @@ func TestDoctorRepair_CorruptDatabase_NotADatabase_RebuildFromJSONL(t *testing.T
}
func TestDoctorRepair_CorruptDatabase_NoJSONL_FixFails(t *testing.T) {
requireTestGuardDisabled(t)
bdExe := buildBDForTest(t)
ws := mkTmpDirInTmp(t, "bd-doctor-chaos-nojsonl-*")
dbPath := filepath.Join(ws, ".beads", "beads.db")
@@ -86,6 +88,7 @@ func TestDoctorRepair_CorruptDatabase_NoJSONL_FixFails(t *testing.T) {
}
func TestDoctorRepair_CorruptDatabase_BacksUpSidecars(t *testing.T) {
requireTestGuardDisabled(t)
bdExe := buildBDForTest(t)
ws := mkTmpDirInTmp(t, "bd-doctor-chaos-sidecars-*")
dbPath := filepath.Join(ws, ".beads", "beads.db")
@@ -141,6 +144,7 @@ func TestDoctorRepair_CorruptDatabase_BacksUpSidecars(t *testing.T) {
}
func TestDoctorRepair_CorruptDatabase_WithRunningDaemon_FixSucceeds(t *testing.T) {
requireTestGuardDisabled(t)
bdExe := buildBDForTest(t)
ws := mkTmpDirInTmp(t, "bd-doctor-chaos-daemon-*")
dbPath := filepath.Join(ws, ".beads", "beads.db")
@@ -188,6 +192,7 @@ func TestDoctorRepair_CorruptDatabase_WithRunningDaemon_FixSucceeds(t *testing.T
}
func TestDoctorRepair_JSONLIntegrity_MalformedLine_ReexportFromDB(t *testing.T) {
requireTestGuardDisabled(t)
bdExe := buildBDForTest(t)
ws := mkTmpDirInTmp(t, "bd-doctor-chaos-jsonl-*")
dbPath := filepath.Join(ws, ".beads", "beads.db")
@@ -228,6 +233,7 @@ func TestDoctorRepair_JSONLIntegrity_MalformedLine_ReexportFromDB(t *testing.T)
}
func TestDoctorRepair_DatabaseIntegrity_DBWriteLocked_ImportFailsFast(t *testing.T) {
requireTestGuardDisabled(t)
bdExe := buildBDForTest(t)
ws := mkTmpDirInTmp(t, "bd-doctor-chaos-db-locked-*")
dbPath := filepath.Join(ws, ".beads", "beads.db")
@@ -277,6 +283,7 @@ func TestDoctorRepair_DatabaseIntegrity_DBWriteLocked_ImportFailsFast(t *testing
}
func TestDoctorRepair_CorruptDatabase_ReadOnlyBeadsDir_PermissionsFixMakesWritable(t *testing.T) {
requireTestGuardDisabled(t)
bdExe := buildBDForTest(t)
ws := mkTmpDirInTmp(t, "bd-doctor-chaos-readonly-*")
beadsDir := filepath.Join(ws, ".beads")

View File

@@ -60,6 +60,8 @@ func runBDSideDB(t *testing.T, exe, dir, dbPath string, args ...string) (string,
}
func TestDoctorRepair_CorruptDatabase_RebuildFromJSONL(t *testing.T) {
requireTestGuardDisabled(t)
if testing.Short() {
t.Skip("skipping slow repair test in short mode")
}

View File

@@ -440,14 +440,14 @@ func TestCompareVersions(t *testing.T) {
v2 string
expected int
}{
{"0.20.1", "0.20.1", 0}, // Equal
{"0.20.1", "0.20.0", 1}, // v1 > v2
{"0.20.0", "0.20.1", -1}, // v1 < v2
{"0.10.0", "0.9.9", 1}, // Major.minor comparison
{"1.0.0", "0.99.99", 1}, // Major version difference
{"0.20.1", "0.3.0", 1}, // String comparison would fail this
{"1.2", "1.2.0", 0}, // Different length, equal
{"1.2.1", "1.2", 1}, // Different length, v1 > v2
{"0.20.1", "0.20.1", 0}, // Equal
{"0.20.1", "0.20.0", 1}, // v1 > v2
{"0.20.0", "0.20.1", -1}, // v1 < v2
{"0.10.0", "0.9.9", 1}, // Major.minor comparison
{"1.0.0", "0.99.99", 1}, // Major version difference
{"0.20.1", "0.3.0", 1}, // String comparison would fail this
{"1.2", "1.2.0", 0}, // Different length, equal
{"1.2.1", "1.2", 1}, // Different length, v1 > v2
}
for _, tc := range tests {
@@ -598,7 +598,6 @@ func TestCheckDatabaseJSONLSync(t *testing.T) {
}
}
func TestCountJSONLIssuesWithMalformedLines(t *testing.T) {
tmpDir := t.TempDir()
beadsDir := filepath.Join(tmpDir, ".beads")
@@ -687,57 +686,47 @@ func TestCheckGitHooks(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
tmpDir := t.TempDir()
// Always change to tmpDir to ensure GetGitDir detects the correct context
oldDir, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get current directory: %v", err)
}
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to test directory: %v", err)
}
defer func() {
_ = os.Chdir(oldDir)
}()
runInDir(t, tmpDir, func() {
if tc.hasGitDir {
// Initialize a real git repository in the test directory
cmd := exec.Command("git", "init")
cmd.Dir = tmpDir
if err := cmd.Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
if tc.hasGitDir {
// Initialize a real git repository in the test directory
cmd := exec.Command("git", "init")
cmd.Dir = tmpDir
if err := cmd.Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
gitDir, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
hooksDir := filepath.Join(gitDir, "hooks")
if err := os.MkdirAll(hooksDir, 0750); err != nil {
t.Fatal(err)
}
// Create installed hooks
for _, hookName := range tc.installedHooks {
hookPath := filepath.Join(hooksDir, hookName)
if err := os.WriteFile(hookPath, []byte("#!/bin/sh\n"), 0755); err != nil {
gitDir, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
hooksDir := filepath.Join(gitDir, "hooks")
if err := os.MkdirAll(hooksDir, 0750); err != nil {
t.Fatal(err)
}
// Create installed hooks
for _, hookName := range tc.installedHooks {
hookPath := filepath.Join(hooksDir, hookName)
if err := os.WriteFile(hookPath, []byte("#!/bin/sh\n"), 0755); err != nil {
t.Fatal(err)
}
}
}
}
check := doctor.CheckGitHooks()
check := doctor.CheckGitHooks()
if check.Status != tc.expectedStatus {
t.Errorf("Expected status %s, got %s", tc.expectedStatus, check.Status)
}
if check.Status != tc.expectedStatus {
t.Errorf("Expected status %s, got %s", tc.expectedStatus, check.Status)
}
if tc.expectWarning && check.Fix == "" {
t.Error("Expected fix message for warning status")
}
if tc.expectWarning && check.Fix == "" {
t.Error("Expected fix message for warning status")
}
if !tc.expectWarning && check.Fix != "" && tc.hasGitDir {
t.Error("Expected no fix message for non-warning status")
}
if !tc.expectWarning && check.Fix != "" && tc.hasGitDir {
t.Error("Expected no fix message for non-warning status")
}
})
})
}
}
@@ -1009,12 +998,12 @@ func TestIsValidSemver(t *testing.T) {
}{
{"0.24.2", true},
{"1.0.0", true},
{"0.1", true}, // Major.minor is valid
{"1", true}, // Just major is valid
{"", false}, // Empty is invalid
{"invalid", false}, // Non-numeric is invalid
{"0.a.2", false}, // Letters in parts are invalid
{"1.2.3.4", true}, // Extra parts are ok
{"0.1", true}, // Major.minor is valid
{"1", true}, // Just major is valid
{"", false}, // Empty is invalid
{"invalid", false}, // Non-numeric is invalid
{"0.a.2", false}, // Letters in parts are invalid
{"1.2.3.4", true}, // Extra parts are ok
}
for _, tc := range tests {

View File

@@ -0,0 +1,29 @@
package main
import (
"os"
"testing"
"github.com/steveyegge/beads/internal/git"
)
// runInDir changes into dir, resets git caches before/after, and executes fn.
// It ensures tests that mutate git repositories don't leak state across cases.
func runInDir(t *testing.T, dir string, fn func()) {
t.Helper()
origDir, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get working directory: %v", err)
}
if err := os.Chdir(dir); err != nil {
t.Fatalf("failed to change to temp directory: %v", err)
}
git.ResetCaches()
defer func() {
if err := os.Chdir(origDir); err != nil {
t.Fatalf("failed to restore working directory: %v", err)
}
git.ResetCaches()
}()
fn()
}

View File

@@ -34,307 +34,251 @@ func TestGetEmbeddedHooks(t *testing.T) {
}
func TestInstallHooks(t *testing.T) {
// Create temp directory and init git repo
tmpDir := t.TempDir()
// Change to temp directory
t.Chdir(tmpDir)
// Initialize a real git repo (required for git rev-parse)
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
gitDir := filepath.Join(gitDirPath, "hooks")
// Get embedded hooks
hooks, err := getEmbeddedHooks()
if err != nil {
t.Fatalf("getEmbeddedHooks() failed: %v", err)
}
// Install hooks
if err := installHooks(hooks, false, false); err != nil {
t.Fatalf("installHooks() failed: %v", err)
}
// Verify hooks were installed
for hookName := range hooks {
hookPath := filepath.Join(gitDir, hookName)
if _, err := os.Stat(hookPath); os.IsNotExist(err) {
t.Errorf("Hook %s was not installed", hookName)
}
// Windows does not support POSIX executable bits, so skip the check there.
if runtime.GOOS == "windows" {
continue
runInDir(t, tmpDir, func() {
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
info, err := os.Stat(hookPath)
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Errorf("Failed to stat %s: %v", hookName, err)
continue
t.Fatalf("git.GetGitDir() failed: %v", err)
}
if info.Mode()&0111 == 0 {
t.Errorf("Hook %s is not executable", hookName)
gitDir := filepath.Join(gitDirPath, "hooks")
hooks, err := getEmbeddedHooks()
if err != nil {
t.Fatalf("getEmbeddedHooks() failed: %v", err)
}
}
if err := installHooks(hooks, false, false); err != nil {
t.Fatalf("installHooks() failed: %v", err)
}
for hookName := range hooks {
hookPath := filepath.Join(gitDir, hookName)
if _, err := os.Stat(hookPath); os.IsNotExist(err) {
t.Errorf("Hook %s was not installed", hookName)
}
if runtime.GOOS == "windows" {
continue
}
info, err := os.Stat(hookPath)
if err != nil {
t.Errorf("Failed to stat %s: %v", hookName, err)
continue
}
if info.Mode()&0111 == 0 {
t.Errorf("Hook %s is not executable", hookName)
}
}
})
}
func TestInstallHooksBackup(t *testing.T) {
// Create temp directory and init git repo
tmpDir := t.TempDir()
runInDir(t, tmpDir, func() {
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
// Change to temp directory
t.Chdir(tmpDir)
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
gitDir := filepath.Join(gitDirPath, "hooks")
if err := os.MkdirAll(gitDir, 0750); err != nil {
t.Fatalf("Failed to create hooks directory: %v", err)
}
// Initialize a real git repo (required for git rev-parse)
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
existingHook := filepath.Join(gitDir, "pre-commit")
existingContent := "#!/bin/sh\necho old hook\n"
if err := os.WriteFile(existingHook, []byte(existingContent), 0755); err != nil {
t.Fatalf("Failed to create existing hook: %v", err)
}
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
gitDir := filepath.Join(gitDirPath, "hooks")
hooks, err := getEmbeddedHooks()
if err != nil {
t.Fatalf("getEmbeddedHooks() failed: %v", err)
}
// Ensure hooks directory exists
if err := os.MkdirAll(gitDir, 0750); err != nil {
t.Fatalf("Failed to create hooks directory: %v", err)
}
if err := installHooks(hooks, false, false); err != nil {
t.Fatalf("installHooks() failed: %v", err)
}
// Create an existing hook
existingHook := filepath.Join(gitDir, "pre-commit")
existingContent := "#!/bin/sh\necho old hook\n"
if err := os.WriteFile(existingHook, []byte(existingContent), 0755); err != nil {
t.Fatalf("Failed to create existing hook: %v", err)
}
backupPath := existingHook + ".backup"
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
t.Errorf("Backup was not created")
}
// Get embedded hooks
hooks, err := getEmbeddedHooks()
if err != nil {
t.Fatalf("getEmbeddedHooks() failed: %v", err)
}
// Install hooks (should backup existing)
if err := installHooks(hooks, false, false); err != nil {
t.Fatalf("installHooks() failed: %v", err)
}
// Verify backup was created
backupPath := existingHook + ".backup"
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
t.Errorf("Backup was not created")
}
// Verify backup has original content
backupContent, err := os.ReadFile(backupPath)
if err != nil {
t.Fatalf("Failed to read backup: %v", err)
}
if string(backupContent) != existingContent {
t.Errorf("Backup content mismatch: got %q, want %q", string(backupContent), existingContent)
}
backupContent, err := os.ReadFile(backupPath)
if err != nil {
t.Fatalf("Failed to read backup: %v", err)
}
if string(backupContent) != existingContent {
t.Errorf("Backup content mismatch: got %q, want %q", string(backupContent), existingContent)
}
})
}
func TestInstallHooksForce(t *testing.T) {
// Create temp directory and init git repo
tmpDir := t.TempDir()
runInDir(t, tmpDir, func() {
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
// Change to temp directory first, then init
t.Chdir(tmpDir)
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
gitDir := filepath.Join(gitDirPath, "hooks")
if err := os.MkdirAll(gitDir, 0750); err != nil {
t.Fatalf("Failed to create hooks directory: %v", err)
}
// Initialize a real git repo (required for git rev-parse)
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
existingHook := filepath.Join(gitDir, "pre-commit")
if err := os.WriteFile(existingHook, []byte("old"), 0755); err != nil {
t.Fatalf("Failed to create existing hook: %v", err)
}
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
gitDir := filepath.Join(gitDirPath, "hooks")
hooks, err := getEmbeddedHooks()
if err != nil {
t.Fatalf("getEmbeddedHooks() failed: %v", err)
}
// Ensure hooks directory exists
if err := os.MkdirAll(gitDir, 0750); err != nil {
t.Fatalf("Failed to create hooks directory: %v", err)
}
if err := installHooks(hooks, true, false); err != nil {
t.Fatalf("installHooks() failed: %v", err)
}
// Create an existing hook
existingHook := filepath.Join(gitDir, "pre-commit")
if err := os.WriteFile(existingHook, []byte("old"), 0755); err != nil {
t.Fatalf("Failed to create existing hook: %v", err)
}
// Get embedded hooks
hooks, err := getEmbeddedHooks()
if err != nil {
t.Fatalf("getEmbeddedHooks() failed: %v", err)
}
// Install hooks with force (should not create backup)
if err := installHooks(hooks, true, false); err != nil {
t.Fatalf("installHooks() failed: %v", err)
}
// Verify no backup was created
backupPath := existingHook + ".backup"
if _, err := os.Stat(backupPath); !os.IsNotExist(err) {
t.Errorf("Backup should not have been created with --force")
}
backupPath := existingHook + ".backup"
if _, err := os.Stat(backupPath); !os.IsNotExist(err) {
t.Errorf("Backup should not have been created with --force")
}
})
}
func TestUninstallHooks(t *testing.T) {
// Create temp directory and init git repo
tmpDir := t.TempDir()
// Change to temp directory first, then init
t.Chdir(tmpDir)
// Initialize a real git repo (required for git rev-parse)
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
gitDir := filepath.Join(gitDirPath, "hooks")
// Get embedded hooks and install them
hooks, err := getEmbeddedHooks()
if err != nil {
t.Fatalf("getEmbeddedHooks() failed: %v", err)
}
if err := installHooks(hooks, false, false); err != nil {
t.Fatalf("installHooks() failed: %v", err)
}
// Uninstall hooks
if err := uninstallHooks(); err != nil {
t.Fatalf("uninstallHooks() failed: %v", err)
}
// Verify hooks were removed
hookNames := []string{"pre-commit", "post-merge", "pre-push", "post-checkout"}
for _, hookName := range hookNames {
hookPath := filepath.Join(gitDir, hookName)
if _, err := os.Stat(hookPath); !os.IsNotExist(err) {
t.Errorf("Hook %s was not removed", hookName)
runInDir(t, tmpDir, func() {
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
}
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
gitDir := filepath.Join(gitDirPath, "hooks")
hooks, err := getEmbeddedHooks()
if err != nil {
t.Fatalf("getEmbeddedHooks() failed: %v", err)
}
if err := installHooks(hooks, false, false); err != nil {
t.Fatalf("installHooks() failed: %v", err)
}
if err := uninstallHooks(); err != nil {
t.Fatalf("uninstallHooks() failed: %v", err)
}
for hookName := range hooks {
hookPath := filepath.Join(gitDir, hookName)
if _, err := os.Stat(hookPath); !os.IsNotExist(err) {
t.Errorf("Hook %s was not removed", hookName)
}
}
})
}
func TestHooksCheckGitHooks(t *testing.T) {
// Create temp directory and init git repo
tmpDir := t.TempDir()
// Change to temp directory first, then init
t.Chdir(tmpDir)
// Initialize a real git repo (required for git rev-parse)
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
// Initially no hooks installed
statuses := CheckGitHooks()
for _, status := range statuses {
if status.Installed {
t.Errorf("Hook %s should not be installed initially", status.Name)
runInDir(t, tmpDir, func() {
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
}
// Install hooks
hooks, err := getEmbeddedHooks()
if err != nil {
t.Fatalf("getEmbeddedHooks() failed: %v", err)
}
if err := installHooks(hooks, false, false); err != nil {
t.Fatalf("installHooks() failed: %v", err)
}
statuses := CheckGitHooks()
for _, status := range statuses {
if status.Installed {
t.Errorf("Hook %s should not be installed initially", status.Name)
}
}
// Check again
statuses = CheckGitHooks()
hooks, err := getEmbeddedHooks()
if err != nil {
t.Fatalf("getEmbeddedHooks() failed: %v", err)
}
if err := installHooks(hooks, false, false); err != nil {
t.Fatalf("installHooks() failed: %v", err)
}
for _, status := range statuses {
if !status.Installed {
t.Errorf("Hook %s should be installed", status.Name)
statuses = CheckGitHooks()
for _, status := range statuses {
if !status.Installed {
t.Errorf("Hook %s should be installed", status.Name)
}
if !status.IsShim {
t.Errorf("Hook %s should be a thin shim", status.Name)
}
if status.Version != "v1" {
t.Errorf("Hook %s shim version mismatch: got %s, want v1", status.Name, status.Version)
}
if status.Outdated {
t.Errorf("Hook %s should not be outdated", status.Name)
}
}
// Thin shims use version format "v1" (shim format version, not bd version)
if !status.IsShim {
t.Errorf("Hook %s should be a thin shim", status.Name)
}
if status.Version != "v1" {
t.Errorf("Hook %s shim version mismatch: got %s, want v1", status.Name, status.Version)
}
if status.Outdated {
t.Errorf("Hook %s should not be outdated", status.Name)
}
}
})
}
func TestInstallHooksShared(t *testing.T) {
// Create temp directory
tmpDir := t.TempDir()
// Change to temp directory
t.Chdir(tmpDir)
// Initialize a real git repo (needed for git config command)
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed (git may not be available): %v", err)
}
// Get embedded hooks
hooks, err := getEmbeddedHooks()
if err != nil {
t.Fatalf("getEmbeddedHooks() failed: %v", err)
}
// Install hooks in shared mode
if err := installHooks(hooks, false, true); err != nil {
t.Fatalf("installHooks() with shared=true failed: %v", err)
}
// Verify hooks were installed to .beads-hooks/
sharedHooksDir := ".beads-hooks"
for hookName := range hooks {
hookPath := filepath.Join(sharedHooksDir, hookName)
if _, err := os.Stat(hookPath); os.IsNotExist(err) {
t.Errorf("Hook %s was not installed to .beads-hooks/", hookName)
}
// Windows does not support POSIX executable bits, so skip the check there.
if runtime.GOOS == "windows" {
continue
runInDir(t, tmpDir, func() {
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed (git may not be available): %v", err)
}
info, err := os.Stat(hookPath)
hooks, err := getEmbeddedHooks()
if err != nil {
t.Errorf("Failed to stat %s: %v", hookName, err)
continue
t.Fatalf("getEmbeddedHooks() failed: %v", err)
}
if info.Mode()&0111 == 0 {
t.Errorf("Hook %s is not executable", hookName)
}
}
// Verify hooks were NOT installed to .git/hooks/
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
standardHooksDir := filepath.Join(gitDirPath, "hooks")
for hookName := range hooks {
hookPath := filepath.Join(standardHooksDir, hookName)
if _, err := os.Stat(hookPath); !os.IsNotExist(err) {
t.Errorf("Hook %s should not be in .git/hooks/ when using --shared", hookName)
if err := installHooks(hooks, false, true); err != nil {
t.Fatalf("installHooks() with shared=true failed: %v", err)
}
}
sharedHooksDir := ".beads-hooks"
for hookName := range hooks {
hookPath := filepath.Join(sharedHooksDir, hookName)
if _, err := os.Stat(hookPath); os.IsNotExist(err) {
t.Errorf("Hook %s was not installed to .beads-hooks/", hookName)
}
if runtime.GOOS == "windows" {
continue
}
info, err := os.Stat(hookPath)
if err != nil {
t.Errorf("Failed to stat %s: %v", hookName, err)
continue
}
if info.Mode()&0111 == 0 {
t.Errorf("Hook %s is not executable", hookName)
}
}
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
standardHooksDir := filepath.Join(gitDirPath, "hooks")
for hookName := range hooks {
hookPath := filepath.Join(standardHooksDir, hookName)
if _, err := os.Stat(hookPath); !os.IsNotExist(err) {
t.Errorf("Hook %s should not be in .git/hooks/ when using --shared", hookName)
}
}
})
}

View File

@@ -11,181 +11,171 @@ import (
)
func TestDetectExistingHooks(t *testing.T) {
// Create a temporary directory
tmpDir := t.TempDir()
t.Chdir(tmpDir)
runInDir(t, tmpDir, func() {
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
// Initialize a real git repo (required for git rev-parse)
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
hooksDir := filepath.Join(gitDirPath, "hooks")
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
hooksDir := filepath.Join(gitDirPath, "hooks")
tests := []struct {
name string
setupHook string
hookContent string
wantExists bool
wantIsBdHook bool
wantIsPreCommit bool
}{
{
name: "no hook",
setupHook: "",
wantExists: false,
},
{
name: "bd hook",
setupHook: "pre-commit",
hookContent: "#!/bin/sh\n# bd (beads) pre-commit hook\necho test",
wantExists: true,
wantIsBdHook: true,
},
{
name: "pre-commit framework hook",
setupHook: "pre-commit",
hookContent: "#!/bin/sh\n# pre-commit framework\npre-commit run",
wantExists: true,
wantIsPreCommit: true,
},
{
name: "custom hook",
setupHook: "pre-commit",
hookContent: "#!/bin/sh\necho custom",
wantExists: true,
},
}
tests := []struct {
name string
setupHook string
hookContent string
wantExists bool
wantIsBdHook bool
wantIsPreCommit bool
}{
{
name: "no hook",
setupHook: "",
wantExists: false,
},
{
name: "bd hook",
setupHook: "pre-commit",
hookContent: "#!/bin/sh\n# bd (beads) pre-commit hook\necho test",
wantExists: true,
wantIsBdHook: true,
},
{
name: "pre-commit framework hook",
setupHook: "pre-commit",
hookContent: "#!/bin/sh\n# pre-commit framework\npre-commit run",
wantExists: true,
wantIsPreCommit: true,
},
{
name: "custom hook",
setupHook: "pre-commit",
hookContent: "#!/bin/sh\necho custom",
wantExists: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
os.RemoveAll(hooksDir)
os.MkdirAll(hooksDir, 0750)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clean up hooks directory
os.RemoveAll(hooksDir)
os.MkdirAll(hooksDir, 0750)
// Setup hook if needed
if tt.setupHook != "" {
hookPath := filepath.Join(hooksDir, tt.setupHook)
if err := os.WriteFile(hookPath, []byte(tt.hookContent), 0700); err != nil {
t.Fatal(err)
if tt.setupHook != "" {
hookPath := filepath.Join(hooksDir, tt.setupHook)
if err := os.WriteFile(hookPath, []byte(tt.hookContent), 0700); err != nil {
t.Fatal(err)
}
}
}
// Detect hooks
hooks := detectExistingHooks()
hooks := detectExistingHooks()
// Find the hook we're testing
var found *hookInfo
for i := range hooks {
if hooks[i].name == "pre-commit" {
found = &hooks[i]
break
var found *hookInfo
for i := range hooks {
if hooks[i].name == "pre-commit" {
found = &hooks[i]
break
}
}
}
if found == nil {
t.Fatal("pre-commit hook not found in results")
}
if found == nil {
t.Fatal("pre-commit hook not found in results")
}
if found.exists != tt.wantExists {
t.Errorf("exists = %v, want %v", found.exists, tt.wantExists)
}
if found.isBdHook != tt.wantIsBdHook {
t.Errorf("isBdHook = %v, want %v", found.isBdHook, tt.wantIsBdHook)
}
if found.isPreCommit != tt.wantIsPreCommit {
t.Errorf("isPreCommit = %v, want %v", found.isPreCommit, tt.wantIsPreCommit)
}
})
}
if found.exists != tt.wantExists {
t.Errorf("exists = %v, want %v", found.exists, tt.wantExists)
}
if found.isBdHook != tt.wantIsBdHook {
t.Errorf("isBdHook = %v, want %v", found.isBdHook, tt.wantIsBdHook)
}
if found.isPreCommit != tt.wantIsPreCommit {
t.Errorf("isPreCommit = %v, want %v", found.isPreCommit, tt.wantIsPreCommit)
}
})
}
})
}
func TestInstallGitHooks_NoExistingHooks(t *testing.T) {
// Create a temporary directory
tmpDir := t.TempDir()
t.Chdir(tmpDir)
// Initialize a real git repo (required for git rev-parse)
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
hooksDir := filepath.Join(gitDirPath, "hooks")
// Note: Can't fully test interactive prompt in automated tests
// This test verifies the logic works when no existing hooks present
// For full testing, we'd need to mock user input
// Check hooks were created
preCommitPath := filepath.Join(hooksDir, "pre-commit")
postMergePath := filepath.Join(hooksDir, "post-merge")
if _, err := os.Stat(preCommitPath); err == nil {
content, _ := os.ReadFile(preCommitPath)
if !strings.Contains(string(content), "bd (beads)") {
t.Error("pre-commit hook doesn't contain bd marker")
runInDir(t, tmpDir, func() {
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
if strings.Contains(string(content), "chained") {
t.Error("pre-commit hook shouldn't be chained when no existing hooks")
}
}
if _, err := os.Stat(postMergePath); err == nil {
content, _ := os.ReadFile(postMergePath)
if !strings.Contains(string(content), "bd (beads)") {
t.Error("post-merge hook doesn't contain bd marker")
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
}
hooksDir := filepath.Join(gitDirPath, "hooks")
// Note: Can't fully test interactive prompt in automated tests
// This test verifies the logic works when no existing hooks present
// For full testing, we'd need to mock user input
// Check hooks were created
preCommitPath := filepath.Join(hooksDir, "pre-commit")
postMergePath := filepath.Join(hooksDir, "post-merge")
if _, err := os.Stat(preCommitPath); err == nil {
content, _ := os.ReadFile(preCommitPath)
if !strings.Contains(string(content), "bd (beads)") {
t.Error("pre-commit hook doesn't contain bd marker")
}
if strings.Contains(string(content), "chained") {
t.Error("pre-commit hook shouldn't be chained when no existing hooks")
}
}
if _, err := os.Stat(postMergePath); err == nil {
content, _ := os.ReadFile(postMergePath)
if !strings.Contains(string(content), "bd (beads)") {
t.Error("post-merge hook doesn't contain bd marker")
}
}
})
}
func TestInstallGitHooks_ExistingHookBackup(t *testing.T) {
// Create a temporary directory
tmpDir := t.TempDir()
t.Chdir(tmpDir)
// Initialize a real git repo (required for git rev-parse)
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
hooksDir := filepath.Join(gitDirPath, "hooks")
// Ensure hooks directory exists
if err := os.MkdirAll(hooksDir, 0750); err != nil {
t.Fatalf("Failed to create hooks directory: %v", err)
}
// Create an existing pre-commit hook
preCommitPath := filepath.Join(hooksDir, "pre-commit")
existingContent := "#!/bin/sh\necho existing hook"
if err := os.WriteFile(preCommitPath, []byte(existingContent), 0700); err != nil {
t.Fatal(err)
}
// Detect that hook exists
hooks := detectExistingHooks()
hasExisting := false
for _, hook := range hooks {
if hook.exists && !hook.isBdHook && hook.name == "pre-commit" {
hasExisting = true
break
runInDir(t, tmpDir, func() {
if err := exec.Command("git", "init").Run(); err != nil {
t.Skipf("Skipping test: git init failed: %v", err)
}
}
if !hasExisting {
t.Error("should detect existing non-bd hook")
}
gitDirPath, err := git.GetGitDir()
if err != nil {
t.Fatalf("git.GetGitDir() failed: %v", err)
}
hooksDir := filepath.Join(gitDirPath, "hooks")
// Ensure hooks directory exists
if err := os.MkdirAll(hooksDir, 0750); err != nil {
t.Fatalf("Failed to create hooks directory: %v", err)
}
// Create an existing pre-commit hook
preCommitPath := filepath.Join(hooksDir, "pre-commit")
existingContent := "#!/bin/sh\necho existing hook"
if err := os.WriteFile(preCommitPath, []byte(existingContent), 0700); err != nil {
t.Fatal(err)
}
// Detect that hook exists
hooks := detectExistingHooks()
hasExisting := false
for _, hook := range hooks {
if hook.exists && !hook.isBdHook && hook.name == "pre-commit" {
hasExisting = true
break
}
}
if !hasExisting {
t.Error("should detect existing non-bd hook")
}
})
}

View File

@@ -0,0 +1,17 @@
package main
import (
"os"
"testing"
)
// requireTestGuardDisabled skips destructive integration tests unless the
// BEADS_TEST_GUARD_DISABLE flag is set, mirroring the behavior enforced by the
// guard when running the full suite.
func requireTestGuardDisabled(t *testing.T) {
t.Helper()
if os.Getenv("BEADS_TEST_GUARD_DISABLE") != "" {
return
}
t.Skip("set BEADS_TEST_GUARD_DISABLE=1 to run this integration test")
}