@@ -3,6 +3,7 @@ package main
33import (
44 "fmt"
55 "os"
6+ "os/exec"
67 "path/filepath"
78 "strings"
89 "testing"
@@ -687,6 +688,114 @@ func TestUninstallHooksRemovesEmptyFile(t *testing.T) {
687688 })
688689}
689690
691+ // TestConfigureBeadsHooksPath_AbsolutePath verifies that core.hooksPath is set to
692+ // an absolute path so that git worktrees can find the hooks directory (GH#2414).
693+ func TestConfigureBeadsHooksPath_AbsolutePath (t * testing.T ) {
694+ tmpDir := newGitRepo (t )
695+ runInDir (t , tmpDir , func () {
696+ // Create .beads/hooks/ directory
697+ beadsHooksDir := filepath .Join (tmpDir , ".beads" , "hooks" )
698+ if err := os .MkdirAll (beadsHooksDir , 0750 ); err != nil {
699+ t .Fatalf ("Failed to create .beads/hooks/: %v" , err )
700+ }
701+
702+ if err := configureBeadsHooksPath (); err != nil {
703+ t .Fatalf ("configureBeadsHooksPath() failed: %v" , err )
704+ }
705+
706+ // Read back core.hooksPath
707+ out , err := exec .Command ("git" , "config" , "--get" , "core.hooksPath" ).Output ()
708+ if err != nil {
709+ t .Fatalf ("git config --get core.hooksPath failed: %v" , err )
710+ }
711+ hooksPath := strings .TrimSpace (string (out ))
712+
713+ // Must be absolute
714+ if ! filepath .IsAbs (hooksPath ) {
715+ t .Errorf ("core.hooksPath should be absolute, got %q" , hooksPath )
716+ }
717+
718+ // Must point to .beads/hooks
719+ if ! strings .HasSuffix (hooksPath , filepath .Join (".beads" , "hooks" )) {
720+ t .Errorf ("core.hooksPath should end with .beads/hooks, got %q" , hooksPath )
721+ }
722+ })
723+ }
724+
725+ // TestInstallHooksBeads_WorktreeAccess verifies that hooks installed with --beads
726+ // are accessible from a git worktree (GH#2414).
727+ func TestInstallHooksBeads_WorktreeAccess (t * testing.T ) {
728+ tmpDir := newGitRepo (t )
729+ runInDir (t , tmpDir , func () {
730+ // Create .beads/ directory with metadata.json (needed for FindBeadsDir)
731+ beadsDir := filepath .Join (tmpDir , ".beads" )
732+ if err := os .MkdirAll (beadsDir , 0750 ); err != nil {
733+ t .Fatalf ("Failed to create .beads/: %v" , err )
734+ }
735+ if err := os .WriteFile (filepath .Join (beadsDir , "metadata.json" ), []byte (`{}` ), 0644 ); err != nil {
736+ t .Fatalf ("Failed to create metadata.json: %v" , err )
737+ }
738+
739+ // Install hooks with --beads
740+ if err := installHooksWithOptions (managedHookNames , false , false , false , true ); err != nil {
741+ t .Fatalf ("installHooksWithOptions(beads=true) failed: %v" , err )
742+ }
743+
744+ // Verify hooks exist in .beads/hooks/
745+ for _ , hookName := range managedHookNames {
746+ hookPath := filepath .Join (beadsDir , "hooks" , hookName )
747+ if _ , err := os .Stat (hookPath ); err != nil {
748+ t .Errorf ("hook %s not found at %s" , hookName , hookPath )
749+ }
750+ }
751+
752+ // Read core.hooksPath and verify it's absolute
753+ out , err := exec .Command ("git" , "config" , "--get" , "core.hooksPath" ).Output ()
754+ if err != nil {
755+ t .Fatalf ("core.hooksPath not set after --beads install: %v" , err )
756+ }
757+ hooksPath := strings .TrimSpace (string (out ))
758+ if ! filepath .IsAbs (hooksPath ) {
759+ t .Errorf ("core.hooksPath should be absolute for worktree compatibility, got %q" , hooksPath )
760+ }
761+
762+ // Create a worktree and verify hooks are accessible from it
763+ worktreeDir := filepath .Join (t .TempDir (), "worktree" )
764+ cmd := exec .Command ("git" , "worktree" , "add" , worktreeDir , "-b" , "test-worktree" )
765+ cmd .Dir = tmpDir
766+ if output , err := cmd .CombinedOutput (); err != nil {
767+ t .Fatalf ("git worktree add failed: %v\n %s" , err , string (output ))
768+ }
769+ defer func () {
770+ exec .Command ("git" , "worktree" , "remove" , worktreeDir ).Run ()
771+ }()
772+
773+ // From the worktree, core.hooksPath should resolve to the same hooks
774+ cmd = exec .Command ("git" , "config" , "--get" , "core.hooksPath" )
775+ cmd .Dir = worktreeDir
776+ wtOut , err := cmd .Output ()
777+ if err != nil {
778+ t .Fatalf ("core.hooksPath not visible from worktree: %v" , err )
779+ }
780+ wtHooksPath := strings .TrimSpace (string (wtOut ))
781+
782+ if wtHooksPath != hooksPath {
783+ t .Errorf ("worktree core.hooksPath = %q, want %q" , wtHooksPath , hooksPath )
784+ }
785+
786+ // The hooks directory must actually exist at the resolved path
787+ if _ , err := os .Stat (wtHooksPath ); err != nil {
788+ t .Errorf ("hooks directory not accessible from worktree at %q: %v" , wtHooksPath , err )
789+ }
790+
791+ // Verify a specific hook file exists
792+ preCommitPath := filepath .Join (wtHooksPath , "pre-commit" )
793+ if _ , err := os .Stat (preCommitPath ); err != nil {
794+ t .Errorf ("pre-commit hook not accessible from worktree: %v" , err )
795+ }
796+ })
797+ }
798+
690799func TestHooksNeedUpdate (t * testing.T ) {
691800 tests := []struct {
692801 name string
0 commit comments