diff --git a/git/service_extra_test.go b/git/service_extra_test.go new file mode 100644 index 0000000..13cd0e6 --- /dev/null +++ b/git/service_extra_test.go @@ -0,0 +1,287 @@ +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) +}