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>
287 lines
7 KiB
Go
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)
|
|
}
|