test: expand routing and compact coverage

This commit is contained in:
Jordan Hubbard
2025-12-29 00:55:44 -04:00
committed by Steve Yegge
parent cb280b0fad
commit 12b797781d
4 changed files with 168 additions and 40 deletions

View File

@@ -1,21 +1,20 @@
package compact
import (
"bytes"
"os/exec"
"strings"
)
var gitExec = func(name string, args ...string) ([]byte, error) {
return exec.Command(name, args...).Output()
}
// GetCurrentCommitHash returns the current git HEAD commit hash.
// Returns empty string if not in a git repository or if git command fails.
func GetCurrentCommitHash() string {
cmd := exec.Command("git", "rev-parse", "HEAD")
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
output, err := gitExec("git", "rev-parse", "HEAD")
if err != nil {
return ""
}
return strings.TrimSpace(out.String())
return strings.TrimSpace(string(output))
}

View File

@@ -0,0 +1,30 @@
package compact
import (
"errors"
"testing"
)
func TestGetCurrentCommitHashSuccess(t *testing.T) {
orig := gitExec
gitExec = func(string, ...string) ([]byte, error) {
return []byte("abc123\n"), nil
}
t.Cleanup(func() { gitExec = orig })
if got := GetCurrentCommitHash(); got != "abc123" {
t.Fatalf("expected trimmed hash, got %q", got)
}
}
func TestGetCurrentCommitHashError(t *testing.T) {
orig := gitExec
gitExec = func(string, ...string) ([]byte, error) {
return nil, errors.New("boom")
}
t.Cleanup(func() { gitExec = orig })
if got := GetCurrentCommitHash(); got != "" {
t.Fatalf("expected empty string on error, got %q", got)
}
}

View File

@@ -5,6 +5,14 @@ import (
"strings"
)
var gitCommandRunner = func(repoPath string, args ...string) ([]byte, error) {
cmd := exec.Command("git", args...)
if repoPath != "" {
cmd.Dir = repoPath
}
return cmd.Output()
}
// UserRole represents whether the user is a maintainer or contributor
type UserRole string
@@ -22,11 +30,7 @@ const (
// 3. Fall back to contributor if uncertain
func DetectUserRole(repoPath string) (UserRole, error) {
// First check for explicit role in git config
cmd := exec.Command("git", "config", "--get", "beads.role")
if repoPath != "" {
cmd.Dir = repoPath
}
output, err := cmd.Output()
output, err := gitCommandRunner(repoPath, "config", "--get", "beads.role")
if err == nil {
role := strings.TrimSpace(string(output))
if role == string(Maintainer) {
@@ -38,18 +42,10 @@ func DetectUserRole(repoPath string) (UserRole, error) {
}
// Check push access by examining remote URL
cmd = exec.Command("git", "remote", "get-url", "--push", "origin")
if repoPath != "" {
cmd.Dir = repoPath
}
output, err = cmd.Output()
output, err = gitCommandRunner(repoPath, "remote", "get-url", "--push", "origin")
if err != nil {
// Fallback to standard fetch URL if push URL fails (some git versions/configs)
cmd = exec.Command("git", "remote", "get-url", "origin")
if repoPath != "" {
cmd.Dir = repoPath
}
output, err = cmd.Output()
output, err = gitCommandRunner(repoPath, "remote", "get-url", "origin")
if err != nil {
// No remote or error - default to contributor
return Contributor, nil
@@ -57,13 +53,13 @@ func DetectUserRole(repoPath string) (UserRole, error) {
}
pushURL := strings.TrimSpace(string(output))
// Check if URL indicates write access
// SSH URLs (git@github.com:user/repo.git) typically indicate write access
// HTTPS with token/password also indicates write access
if strings.HasPrefix(pushURL, "git@") ||
strings.HasPrefix(pushURL, "ssh://") ||
strings.Contains(pushURL, "@") {
if strings.HasPrefix(pushURL, "git@") ||
strings.HasPrefix(pushURL, "ssh://") ||
strings.Contains(pushURL, "@") {
return Maintainer, nil
}
@@ -73,11 +69,11 @@ func DetectUserRole(repoPath string) (UserRole, error) {
// RoutingConfig defines routing rules for issues
type RoutingConfig struct {
Mode string // "auto" or "explicit"
DefaultRepo string // Default repo for new issues
MaintainerRepo string // Repo for maintainers (in auto mode)
ContributorRepo string // Repo for contributors (in auto mode)
ExplicitOverride string // Explicit --repo flag override
Mode string // "auto" or "explicit"
DefaultRepo string // Default repo for new issues
MaintainerRepo string // Repo for maintainers (in auto mode)
ContributorRepo string // Repo for contributors (in auto mode)
ExplicitOverride string // Explicit --repo flag override
}
// DetermineTargetRepo determines which repo should receive a new issue

View File

@@ -1,6 +1,8 @@
package routing
import (
"errors"
"reflect"
"testing"
)
@@ -15,11 +17,11 @@ func TestDetermineTargetRepo(t *testing.T) {
{
name: "explicit override takes precedence",
config: &RoutingConfig{
Mode: "auto",
DefaultRepo: "~/planning",
MaintainerRepo: ".",
ContributorRepo: "~/contributor-planning",
ExplicitOverride: "/tmp/custom",
Mode: "auto",
DefaultRepo: "~/planning",
MaintainerRepo: ".",
ContributorRepo: "~/contributor-planning",
ExplicitOverride: "/tmp/custom",
},
userRole: Maintainer,
repoPath: ".",
@@ -97,9 +99,9 @@ func TestExtractPrefix(t *testing.T) {
{"gt-abc123", "gt-"},
{"bd-xyz", "bd-"},
{"hq-1234", "hq-"},
{"abc123", ""}, // No hyphen
{"", ""}, // Empty string
{"-abc", "-"}, // Starts with hyphen
{"abc123", ""}, // No hyphen
{"", ""}, // Empty string
{"-abc", "-"}, // Starts with hyphen
}
for _, tt := range tests {
@@ -142,3 +144,104 @@ func TestResolveToExternalRef(t *testing.T) {
t.Errorf("ResolveToExternalRef() = %q, want empty string for nonexistent path", got)
}
}
type gitCall struct {
repo string
args []string
}
type gitResponse struct {
expect gitCall
output string
err error
}
type gitStub struct {
t *testing.T
responses []gitResponse
idx int
}
func (s *gitStub) run(repo string, args ...string) ([]byte, error) {
if s.idx >= len(s.responses) {
s.t.Fatalf("unexpected git call %v in repo %s", args, repo)
}
resp := s.responses[s.idx]
s.idx++
if resp.expect.repo != repo {
s.t.Fatalf("repo mismatch: got %q want %q", repo, resp.expect.repo)
}
if !reflect.DeepEqual(resp.expect.args, args) {
s.t.Fatalf("args mismatch: got %v want %v", args, resp.expect.args)
}
return []byte(resp.output), resp.err
}
func (s *gitStub) verify() {
if s.idx != len(s.responses) {
s.t.Fatalf("expected %d git calls, got %d", len(s.responses), s.idx)
}
}
func TestDetectUserRole_ConfigOverrideMaintainer(t *testing.T) {
orig := gitCommandRunner
stub := &gitStub{t: t, responses: []gitResponse{
{expect: gitCall{"", []string{"config", "--get", "beads.role"}}, output: "maintainer\n"},
}}
gitCommandRunner = stub.run
t.Cleanup(func() {
gitCommandRunner = orig
stub.verify()
})
role, err := DetectUserRole("")
if err != nil {
t.Fatalf("DetectUserRole error = %v", err)
}
if role != Maintainer {
t.Fatalf("expected %s, got %s", Maintainer, role)
}
}
func TestDetectUserRole_PushURLMaintainer(t *testing.T) {
orig := gitCommandRunner
stub := &gitStub{t: t, responses: []gitResponse{
{expect: gitCall{"/repo", []string{"config", "--get", "beads.role"}}, output: "unknown"},
{expect: gitCall{"/repo", []string{"remote", "get-url", "--push", "origin"}}, output: "git@github.com:owner/repo.git"},
}}
gitCommandRunner = stub.run
t.Cleanup(func() {
gitCommandRunner = orig
stub.verify()
})
role, err := DetectUserRole("/repo")
if err != nil {
t.Fatalf("DetectUserRole error = %v", err)
}
if role != Maintainer {
t.Fatalf("expected %s, got %s", Maintainer, role)
}
}
func TestDetectUserRole_DefaultContributor(t *testing.T) {
orig := gitCommandRunner
stub := &gitStub{t: t, responses: []gitResponse{
{expect: gitCall{"", []string{"config", "--get", "beads.role"}}, err: errors.New("missing")},
{expect: gitCall{"", []string{"remote", "get-url", "--push", "origin"}}, err: errors.New("no push")},
{expect: gitCall{"", []string{"remote", "get-url", "origin"}}, output: "https://github.com/owner/repo.git"},
}}
gitCommandRunner = stub.run
t.Cleanup(func() {
gitCommandRunner = orig
stub.verify()
})
role, err := DetectUserRole("")
if err != nil {
t.Fatalf("DetectUserRole error = %v", err)
}
if role != Contributor {
t.Fatalf("expected %s, got %s", Contributor, role)
}
}