package git_test import ( "context" "os" "path/filepath" "strconv" "strings" "testing" "superwork-tui/internal/git" ) func makeHookCtx(workspaceRoot, worktreePath, branch string, issue int, main string) git.HookContext { return git.HookContext{ WorkspaceRoot: workspaceRoot, WorktreePath: worktreePath, Branch: branch, IssueNumber: issue, MainBranch: main, } } func writeScript(t *testing.T, dir, name, content string) string { t.Helper() p := filepath.Join(dir, name) if err := os.WriteFile(p, []byte(content), 0o755); err != nil { t.Fatal(err) } return p } // setupWorkspace returns workspaceRoot with .spx/ created. func setupWorkspace(t *testing.T) (workspaceRoot, spxDir string) { t.Helper() workspaceRoot = t.TempDir() spxDir = filepath.Join(workspaceRoot, ".spx") if err := os.MkdirAll(spxDir, 0o755); err != nil { t.Fatal(err) } return } // --- RunPostCreateHook --- func TestRunPostCreateHook_Missing(t *testing.T) { root, _ := setupWorkspace(t) ctx := makeHookCtx(root, root, "feat/x", 42, "main") // No script on disk → skipped. result := git.RunPostCreateHook(context.Background(), ctx) if result.Status != git.HookStatusSkipped { t.Errorf("want skipped, got %v", result.Status) } } func TestRunPostCreateHook_Ok(t *testing.T) { root, spxDir := setupWorkspace(t) worktree := t.TempDir() // Script writes env vars to a file so we can assert them. envFile := filepath.Join(root, "env-dump.txt") script := `#!/bin/bash echo "WORKTREE_PATH=$WORKTREE_PATH" >> "` + envFile + `" echo "WORKSPACE_ROOT=$WORKSPACE_ROOT" >> "` + envFile + `" echo "BRANCH=$BRANCH" >> "` + envFile + `" echo "ISSUE_NUMBER=$ISSUE_NUMBER" >> "` + envFile + `" echo "MAIN_BRANCH=$MAIN_BRANCH" >> "` + envFile + `" exit 0 ` writeScript(t, spxDir, "worktree-post-create.sh", script) ctx := makeHookCtx(root, worktree, "feat/my-branch", 99, "develop") result := git.RunPostCreateHook(context.Background(), ctx) if result.Status != git.HookStatusOk { t.Fatalf("want ok, got %v (err=%s stderr=%s)", result.Status, result.ErrorMessage, result.Stderr) } if result.ExitCode != 0 { t.Errorf("want exit code 0, got %d", result.ExitCode) } if result.ScriptPath == "" { t.Error("ScriptPath must not be empty") } envData, err := os.ReadFile(envFile) if err != nil { t.Fatalf("env dump not written: %v", err) } env := string(envData) checks := map[string]string{ "WORKTREE_PATH": worktree, "WORKSPACE_ROOT": root, "BRANCH": "feat/my-branch", "ISSUE_NUMBER": "99", "MAIN_BRANCH": "develop", } for k, v := range checks { if !strings.Contains(env, k+"="+v) { t.Errorf("env missing %s=%s; got:\n%s", k, v, env) } } } func TestRunPostCreateHook_Failed(t *testing.T) { root, spxDir := setupWorkspace(t) writeScript(t, spxDir, "worktree-post-create.sh", "#!/bin/bash\nexit 3\n") ctx := makeHookCtx(root, root, "feat/x", 1, "main") result := git.RunPostCreateHook(context.Background(), ctx) if result.Status != git.HookStatusFailed { t.Errorf("want failed, got %v", result.Status) } if result.ExitCode != 3 { t.Errorf("want ExitCode=3, got %d", result.ExitCode) } } // --- RunPreRemoveHook --- func TestRunPreRemoveHook_Missing(t *testing.T) { root, _ := setupWorkspace(t) ctx := makeHookCtx(root, root, "feat/x", 1, "main") result := git.RunPreRemoveHook(context.Background(), ctx) if result.Status != git.HookStatusSkipped { t.Errorf("want skipped, got %v", result.Status) } } func TestRunPreRemoveHook_Ok(t *testing.T) { root, spxDir := setupWorkspace(t) writeScript(t, spxDir, "worktree-pre-remove.sh", "#!/bin/bash\nexit 0\n") ctx := makeHookCtx(root, root, "feat/x", 1, "main") result := git.RunPreRemoveHook(context.Background(), ctx) if result.Status != git.HookStatusOk { t.Errorf("want ok, got %v", result.Status) } } // --- RunImplTabPreCreateHook --- func TestRunImplTabPreCreateHook_Missing(t *testing.T) { root, _ := setupWorkspace(t) ctx := makeHookCtx(root, root, "feat/x", 1, "main") result := git.RunImplTabPreCreateHook(context.Background(), ctx) if result.Status != git.HookStatusSkipped { t.Errorf("want skipped, got %v", result.Status) } } func TestRunImplTabPreCreateHook_Ok(t *testing.T) { root, spxDir := setupWorkspace(t) writeScript(t, spxDir, "impl-tab-pre-create.sh", "#!/bin/bash\nexit 0\n") ctx := makeHookCtx(root, root, "feat/x", 7, "main") result := git.RunImplTabPreCreateHook(context.Background(), ctx) if result.Status != git.HookStatusOk { t.Errorf("want ok, got %v", result.Status) } if result.ExitCode != 0 { t.Errorf("want exit 0, got %d", result.ExitCode) } } func TestRunImplTabPreCreateHook_Failed(t *testing.T) { root, spxDir := setupWorkspace(t) writeScript(t, spxDir, "impl-tab-pre-create.sh", "#!/bin/bash\nexit 7\n") ctx := makeHookCtx(root, root, "feat/x", 1, "main") result := git.RunImplTabPreCreateHook(context.Background(), ctx) if result.Status != git.HookStatusFailed { t.Errorf("want failed, got %v", result.Status) } if result.ExitCode != 7 { t.Errorf("want ExitCode=7, got %d", result.ExitCode) } } // --- RunImplTabPostCloseHook --- func TestRunImplTabPostCloseHook_Missing(t *testing.T) { root, _ := setupWorkspace(t) ctx := makeHookCtx(root, root, "feat/x", 1, "main") result := git.RunImplTabPostCloseHook(context.Background(), ctx) if result.Status != git.HookStatusSkipped { t.Errorf("want skipped, got %v", result.Status) } } func TestRunImplTabPostCloseHook_Ok(t *testing.T) { root, spxDir := setupWorkspace(t) writeScript(t, spxDir, "impl-tab-post-close.sh", "#!/bin/bash\nexit 0\n") ctx := makeHookCtx(root, root, "feat/x", 3, "main") result := git.RunImplTabPostCloseHook(context.Background(), ctx) if result.Status != git.HookStatusOk { t.Errorf("want ok, got %v", result.Status) } } // --- CustomScriptPath override --- func TestRunPostCreateHook_CustomScriptPath(t *testing.T) { root, _ := setupWorkspace(t) customDir := t.TempDir() customScript := filepath.Join(customDir, "my-hook.sh") if err := os.WriteFile(customScript, []byte("#!/bin/bash\nexit 0\n"), 0o755); err != nil { t.Fatal(err) } ctx := git.HookContext{ WorkspaceRoot: root, WorktreePath: root, Branch: "feat/x", IssueNumber: 1, MainBranch: "main", CustomScriptPath: customScript, // absolute override } result := git.RunPostCreateHook(context.Background(), ctx) if result.Status != git.HookStatusOk { t.Errorf("want ok with custom path, got %v", result.Status) } if result.ScriptPath != customScript { t.Errorf("ScriptPath: want %q, got %q", customScript, result.ScriptPath) } } // IssueNumber env value must be the decimal string, not anything else. func TestRunPostCreateHook_IssueNumberEnv(t *testing.T) { root, spxDir := setupWorkspace(t) envFile := filepath.Join(root, "issue-num.txt") script := "#!/bin/bash\necho -n \"$ISSUE_NUMBER\" > \"" + envFile + "\"\nexit 0\n" writeScript(t, spxDir, "worktree-post-create.sh", script) ctx := makeHookCtx(root, root, "feat/x", 12345, "main") result := git.RunPostCreateHook(context.Background(), ctx) if result.Status != git.HookStatusOk { t.Fatalf("want ok, got %v", result.Status) } data, err := os.ReadFile(envFile) if err != nil { t.Fatal(err) } got := strings.TrimSpace(string(data)) if got != strconv.Itoa(12345) { t.Errorf("ISSUE_NUMBER env: want %q, got %q", "12345", got) } }