go-scm/git/service_test.go
Claude 9db37c6fb3
test: add comprehensive unit tests for forge/, gitea/, git/, agentci/
Phase 1 test coverage for the three 0% packages plus agentci/ improvement:

- git/ (0% -> 79.5%): RepoStatus methods, status parsing with real temp
  repos, multi-repo parallel status, Push/Pull error paths, ahead/behind
  with bare remote, context cancellation, GitError, IsNonFastForward,
  service DirtyRepos/AheadRepos filtering

- forge/ (0% -> 91.2%): All SDK wrapper functions tested via httptest mock
  server — client creation, repos, issues, PRs, labels, webhooks, orgs,
  meta, config resolution, SetPRDraft raw HTTP endpoint

- gitea/ (0% -> 89.2%): All SDK wrapper functions tested via httptest mock
  server — client creation, repos, issues, PRs, meta, config resolution

- agentci/ (56% -> 94.5%): Clotho DeterminePlan all code paths, security
  helpers (SanitizePath, EscapeShellArg, SecureSSHCommand, MaskToken)

Key findings documented in FINDINGS.md:
- Forgejo SDK validates token via HTTP on NewClient()
- SDK route patterns differ from public API docs (/org/ vs /orgs/)
- Gitea SDK requires auth token for GitHub mirror creation
- Config resolution priority verified: config file < env vars < flags

Co-Authored-By: Charon <developers@lethean.io>
2026-02-20 00:59:46 +00:00

155 lines
3.5 KiB
Go

package git
import (
"testing"
"github.com/stretchr/testify/assert"
)
// --- Service helper method tests ---
// These test DirtyRepos/AheadRepos filtering without needing the framework.
func TestService_DirtyRepos_Good(t *testing.T) {
s := &Service{
lastStatus: []RepoStatus{
{Name: "clean", Modified: 0, Untracked: 0, Staged: 0},
{Name: "dirty-modified", Modified: 2},
{Name: "dirty-untracked", Untracked: 1},
{Name: "dirty-staged", Staged: 3},
{Name: "errored", Modified: 5, Error: assert.AnError},
},
}
dirty := s.DirtyRepos()
assert.Len(t, dirty, 3)
names := make([]string, len(dirty))
for i, d := range dirty {
names[i] = d.Name
}
assert.Contains(t, names, "dirty-modified")
assert.Contains(t, names, "dirty-untracked")
assert.Contains(t, names, "dirty-staged")
}
func TestService_DirtyRepos_Good_NoneFound(t *testing.T) {
s := &Service{
lastStatus: []RepoStatus{
{Name: "clean1"},
{Name: "clean2"},
},
}
dirty := s.DirtyRepos()
assert.Empty(t, dirty)
}
func TestService_DirtyRepos_Good_EmptyStatus(t *testing.T) {
s := &Service{}
dirty := s.DirtyRepos()
assert.Empty(t, dirty)
}
func TestService_AheadRepos_Good(t *testing.T) {
s := &Service{
lastStatus: []RepoStatus{
{Name: "up-to-date", Ahead: 0},
{Name: "ahead-by-one", Ahead: 1},
{Name: "ahead-by-five", Ahead: 5},
{Name: "behind-only", Behind: 3},
{Name: "errored-ahead", Ahead: 2, Error: assert.AnError},
},
}
ahead := s.AheadRepos()
assert.Len(t, ahead, 2)
names := make([]string, len(ahead))
for i, a := range ahead {
names[i] = a.Name
}
assert.Contains(t, names, "ahead-by-one")
assert.Contains(t, names, "ahead-by-five")
}
func TestService_AheadRepos_Good_NoneFound(t *testing.T) {
s := &Service{
lastStatus: []RepoStatus{
{Name: "synced1"},
{Name: "synced2"},
},
}
ahead := s.AheadRepos()
assert.Empty(t, ahead)
}
func TestService_AheadRepos_Good_EmptyStatus(t *testing.T) {
s := &Service{}
ahead := s.AheadRepos()
assert.Empty(t, ahead)
}
func TestService_Status_Good(t *testing.T) {
expected := []RepoStatus{
{Name: "repo1", Branch: "main"},
{Name: "repo2", Branch: "develop"},
}
s := &Service{lastStatus: expected}
assert.Equal(t, expected, s.Status())
}
func TestService_Status_Good_NilSlice(t *testing.T) {
s := &Service{}
assert.Nil(t, s.Status())
}
// --- Query/Task type tests ---
func TestQueryStatus_MapsToStatusOptions(t *testing.T) {
q := QueryStatus{
Paths: []string{"/path/a", "/path/b"},
Names: map[string]string{"/path/a": "repo-a"},
}
// QueryStatus can be cast directly to StatusOptions.
opts := StatusOptions(q)
assert.Equal(t, q.Paths, opts.Paths)
assert.Equal(t, q.Names, opts.Names)
}
func TestServiceOptions_WorkDir(t *testing.T) {
opts := ServiceOptions{WorkDir: "/home/claude/repos"}
assert.Equal(t, "/home/claude/repos", opts.WorkDir)
}
// --- DirtyRepos excludes errored repos ---
func TestService_DirtyRepos_Good_ExcludesErrors(t *testing.T) {
s := &Service{
lastStatus: []RepoStatus{
{Name: "dirty-ok", Modified: 1},
{Name: "dirty-error", Modified: 1, Error: assert.AnError},
},
}
dirty := s.DirtyRepos()
assert.Len(t, dirty, 1)
assert.Equal(t, "dirty-ok", dirty[0].Name)
}
// --- AheadRepos excludes errored repos ---
func TestService_AheadRepos_Good_ExcludesErrors(t *testing.T) {
s := &Service{
lastStatus: []RepoStatus{
{Name: "ahead-ok", Ahead: 2},
{Name: "ahead-error", Ahead: 3, Error: assert.AnError},
},
}
ahead := s.AheadRepos()
assert.Len(t, ahead, 1)
assert.Equal(t, "ahead-ok", ahead[0].Name)
}