go-scm/git/service_extra_test.go
Claude b3e3ef2efb
test(git): push coverage from 79.5% to 96.7%
Add tests for Service lifecycle: NewService factory, OnStartup, and all
handleQuery/handleTask paths (QueryStatus, QueryDirtyRepos,
QueryAheadRepos, TaskPush, TaskPull, TaskPushMultiple, unknown types).

Add integration tests for Push with a real bare remote (push succeeds,
ahead count drops to zero), PushMultiple with multiple paths, empty
paths, and ahead/behind with no upstream tracking branch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 02:01:12 +00:00

287 lines
7 KiB
Go

package git
import (
"context"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"forge.lthn.ai/core/go/pkg/framework"
)
func TestNewService_Good(t *testing.T) {
opts := ServiceOptions{WorkDir: "/tmp/test"}
factory := NewService(opts)
assert.NotNil(t, factory)
// Create a minimal Core to test the factory.
c, err := framework.New()
require.NoError(t, err)
svc, err := factory(c)
require.NoError(t, err)
assert.NotNil(t, svc)
service, ok := svc.(*Service)
require.True(t, ok)
assert.NotNil(t, service)
}
func TestService_OnStartup_Good(t *testing.T) {
c, err := framework.New()
require.NoError(t, err)
opts := ServiceOptions{WorkDir: "/tmp"}
svc := &Service{
ServiceRuntime: framework.NewServiceRuntime(c, opts),
}
err = svc.OnStartup(context.Background())
assert.NoError(t, err)
}
func TestService_HandleQuery_Good_Status(t *testing.T) {
dir := initTestRepo(t)
c, err := framework.New()
require.NoError(t, err)
svc := &Service{
ServiceRuntime: framework.NewServiceRuntime(c, ServiceOptions{}),
}
// Call handleQuery directly.
result, handled, err := svc.handleQuery(c, QueryStatus{
Paths: []string{dir},
Names: map[string]string{dir: "test-repo"},
})
require.NoError(t, err)
assert.True(t, handled)
statuses, ok := result.([]RepoStatus)
require.True(t, ok)
require.Len(t, statuses, 1)
assert.Equal(t, "test-repo", statuses[0].Name)
// Verify lastStatus was updated.
assert.Len(t, svc.lastStatus, 1)
}
func TestService_HandleQuery_Good_DirtyRepos(t *testing.T) {
c, err := framework.New()
require.NoError(t, err)
svc := &Service{
ServiceRuntime: framework.NewServiceRuntime(c, ServiceOptions{}),
lastStatus: []RepoStatus{
{Name: "clean"},
{Name: "dirty", Modified: 1},
},
}
result, handled, err := svc.handleQuery(c, QueryDirtyRepos{})
require.NoError(t, err)
assert.True(t, handled)
dirty, ok := result.([]RepoStatus)
require.True(t, ok)
assert.Len(t, dirty, 1)
assert.Equal(t, "dirty", dirty[0].Name)
}
func TestService_HandleQuery_Good_AheadRepos(t *testing.T) {
c, err := framework.New()
require.NoError(t, err)
svc := &Service{
ServiceRuntime: framework.NewServiceRuntime(c, ServiceOptions{}),
lastStatus: []RepoStatus{
{Name: "synced"},
{Name: "ahead", Ahead: 3},
},
}
result, handled, err := svc.handleQuery(c, QueryAheadRepos{})
require.NoError(t, err)
assert.True(t, handled)
ahead, ok := result.([]RepoStatus)
require.True(t, ok)
assert.Len(t, ahead, 1)
assert.Equal(t, "ahead", ahead[0].Name)
}
func TestService_HandleQuery_Good_UnknownQuery(t *testing.T) {
c, err := framework.New()
require.NoError(t, err)
svc := &Service{
ServiceRuntime: framework.NewServiceRuntime(c, ServiceOptions{}),
}
result, handled, err := svc.handleQuery(c, "unknown query type")
require.NoError(t, err)
assert.False(t, handled)
assert.Nil(t, result)
}
func TestService_HandleTask_Good_Push(t *testing.T) {
dir := initTestRepo(t)
c, err := framework.New()
require.NoError(t, err)
svc := &Service{
ServiceRuntime: framework.NewServiceRuntime(c, ServiceOptions{}),
}
// Push without a remote will fail, but handleTask should still handle it.
_, handled, err := svc.handleTask(c, TaskPush{Path: dir, Name: "test"})
assert.True(t, handled)
assert.Error(t, err, "push without remote should fail")
}
func TestService_HandleTask_Good_Pull(t *testing.T) {
dir := initTestRepo(t)
c, err := framework.New()
require.NoError(t, err)
svc := &Service{
ServiceRuntime: framework.NewServiceRuntime(c, ServiceOptions{}),
}
_, handled, err := svc.handleTask(c, TaskPull{Path: dir, Name: "test"})
assert.True(t, handled)
assert.Error(t, err, "pull without remote should fail")
}
func TestService_HandleTask_Good_PushMultiple(t *testing.T) {
dir := initTestRepo(t)
c, err := framework.New()
require.NoError(t, err)
svc := &Service{
ServiceRuntime: framework.NewServiceRuntime(c, ServiceOptions{}),
}
result, handled, err := svc.handleTask(c, TaskPushMultiple{
Paths: []string{dir},
Names: map[string]string{dir: "test"},
})
assert.True(t, handled)
assert.NoError(t, err) // PushMultiple does not return errors directly
results, ok := result.([]PushResult)
require.True(t, ok)
assert.Len(t, results, 1)
assert.False(t, results[0].Success) // No remote
}
func TestService_HandleTask_Good_UnknownTask(t *testing.T) {
c, err := framework.New()
require.NoError(t, err)
svc := &Service{
ServiceRuntime: framework.NewServiceRuntime(c, ServiceOptions{}),
}
result, handled, err := svc.handleTask(c, "unknown task")
require.NoError(t, err)
assert.False(t, handled)
assert.Nil(t, result)
}
// --- Additional git operation tests ---
func TestGetStatus_Good_AheadBehindNoUpstream(t *testing.T) {
// A repo without a tracking branch should return 0 ahead/behind.
dir := initTestRepo(t)
status := getStatus(context.Background(), dir, "no-upstream")
require.NoError(t, status.Error)
assert.Equal(t, 0, status.Ahead)
assert.Equal(t, 0, status.Behind)
}
func TestPushMultiple_Good_Empty(t *testing.T) {
results := PushMultiple(context.Background(), []string{}, map[string]string{})
assert.Empty(t, results)
}
func TestPushMultiple_Good_MultiplePaths(t *testing.T) {
dir1 := initTestRepo(t)
dir2 := initTestRepo(t)
results := PushMultiple(context.Background(), []string{dir1, dir2}, map[string]string{
dir1: "repo-1",
dir2: "repo-2",
})
require.Len(t, results, 2)
assert.Equal(t, "repo-1", results[0].Name)
assert.Equal(t, "repo-2", results[1].Name)
// Both should fail (no remote).
assert.False(t, results[0].Success)
assert.False(t, results[1].Success)
}
func TestPush_Good_WithRemote(t *testing.T) {
// Create a bare remote and a clone.
bareDir := t.TempDir()
cloneDir := t.TempDir()
cmd := exec.Command("git", "init", "--bare")
cmd.Dir = bareDir
require.NoError(t, cmd.Run())
cmd = exec.Command("git", "clone", bareDir, cloneDir)
require.NoError(t, cmd.Run())
for _, args := range [][]string{
{"git", "config", "user.email", "test@example.com"},
{"git", "config", "user.name", "Test User"},
} {
cmd = exec.Command(args[0], args[1:]...)
cmd.Dir = cloneDir
require.NoError(t, cmd.Run())
}
require.NoError(t, os.WriteFile(filepath.Join(cloneDir, "file.txt"), []byte("v1"), 0644))
for _, args := range [][]string{
{"git", "add", "."},
{"git", "commit", "-m", "initial"},
{"git", "push", "origin", "HEAD"},
} {
cmd = exec.Command(args[0], args[1:]...)
cmd.Dir = cloneDir
out, err := cmd.CombinedOutput()
require.NoError(t, err, "failed: %v: %s", args, string(out))
}
// Make a local commit.
require.NoError(t, os.WriteFile(filepath.Join(cloneDir, "file.txt"), []byte("v2"), 0644))
for _, args := range [][]string{
{"git", "add", "."},
{"git", "commit", "-m", "second commit"},
} {
cmd = exec.Command(args[0], args[1:]...)
cmd.Dir = cloneDir
require.NoError(t, cmd.Run())
}
// Push should succeed.
err := Push(context.Background(), cloneDir)
assert.NoError(t, err)
// Verify ahead count is now 0.
ahead, _ := getAheadBehind(context.Background(), cloneDir)
assert.Equal(t, 0, ahead)
}