gito/services/core/internal/gitengine/command_test.go
toki c6102d33d0 feat: platformless git operation engine implementation
- Update control plane phase roadmap
- Move repo-registry milestone to archive
- Implement git engine command execution
- Add postgres repository storage layer
- Update contracts documentation
- Add test cases for storage and git engine
2026-06-13 21:59:14 +09:00

526 lines
15 KiB
Go

package gitengine
import (
"errors"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"testing"
)
type fakeRunner struct {
outputs []string
errs []error
calls []fakeCall
}
type fakeCall struct {
workdir string
args []string
}
func (f *fakeRunner) Run(workdir string, args ...string) (string, error) {
f.calls = append(f.calls, fakeCall{
workdir: workdir,
args: append([]string(nil), args...),
})
callIndex := len(f.calls) - 1
var output string
if callIndex < len(f.outputs) {
output = f.outputs[callIndex]
}
var err error
if callIndex < len(f.errs) {
err = f.errs[callIndex]
}
return output, err
}
func TestStatusClean(t *testing.T) {
runner := &fakeRunner{outputs: []string{"\n"}}
result, err := Status(runner, "/repo")
if err != nil {
t.Fatalf("Status: %v", err)
}
if !result.Clean {
t.Fatal("expected clean status")
}
if !reflect.DeepEqual(runner.calls[0].args, []string{"status", "--porcelain"}) {
t.Fatalf("args: %#v", runner.calls[0].args)
}
}
func TestDiff(t *testing.T) {
runner := &fakeRunner{outputs: []string{"diff output"}}
got, err := Diff(runner, "/repo", DiffOptions{})
if err != nil {
t.Fatalf("Diff: %v", err)
}
if got != "diff output" {
t.Fatalf("diff output: got %q", got)
}
want := []fakeCall{
{workdir: "/repo", args: []string{"diff"}},
}
if !reflect.DeepEqual(runner.calls, want) {
t.Fatalf("calls:\n got %#v\nwant %#v", runner.calls, want)
}
}
func TestDiffStagedPathspecs(t *testing.T) {
runner := &fakeRunner{}
_, err := Diff(runner, "/repo", DiffOptions{
Staged: true,
Pathspecs: []string{" README.md ", "", "services/core/main.go"},
})
if err != nil {
t.Fatalf("Diff: %v", err)
}
want := []fakeCall{
{workdir: "/repo", args: []string{"diff", "--cached", "--", "README.md", "services/core/main.go"}},
}
if !reflect.DeepEqual(runner.calls, want) {
t.Fatalf("calls:\n got %#v\nwant %#v", runner.calls, want)
}
}
func TestDiffRequiresWorkdir(t *testing.T) {
_, err := Diff(&fakeRunner{}, "", DiffOptions{})
if !errors.Is(err, ErrInvalidGitInput) {
t.Fatalf("expected ErrInvalidGitInput, got %v", err)
}
}
func TestCloneFetchCheckout(t *testing.T) {
runner := &fakeRunner{}
if err := Clone(runner, CloneOptions{
RemoteURL: "ssh://git.example/nomadcode.git",
WorktreePath: "/workspace/nomadcode/000",
Branch: "develop",
}); err != nil {
t.Fatalf("Clone: %v", err)
}
if err := Fetch(runner, "/workspace/nomadcode/000", ""); err != nil {
t.Fatalf("Fetch: %v", err)
}
if err := Checkout(runner, "/workspace/nomadcode/000", "feature/a"); err != nil {
t.Fatalf("Checkout: %v", err)
}
want := []fakeCall{
{args: []string{"clone", "--branch", "develop", "ssh://git.example/nomadcode.git", "/workspace/nomadcode/000"}},
{workdir: "/workspace/nomadcode/000", args: []string{"fetch", "--prune", "origin"}},
{workdir: "/workspace/nomadcode/000", args: []string{"checkout", "feature/a"}},
}
if !reflect.DeepEqual(runner.calls, want) {
t.Fatalf("calls:\n got %#v\nwant %#v", runner.calls, want)
}
}
func TestCommitAndPush(t *testing.T) {
runner := &fakeRunner{}
if err := Commit(runner, "/repo", CommitOptions{Message: "sync roadmap", All: true}); err != nil {
t.Fatalf("Commit: %v", err)
}
if err := Push(runner, "/repo", "", "develop"); err != nil {
t.Fatalf("Push: %v", err)
}
want := []fakeCall{
{workdir: "/repo", args: []string{"add", "-A"}},
{workdir: "/repo", args: []string{"commit", "-m", "sync roadmap"}},
{workdir: "/repo", args: []string{"push", "origin", "develop"}},
}
if !reflect.DeepEqual(runner.calls, want) {
t.Fatalf("calls:\n got %#v\nwant %#v", runner.calls, want)
}
}
func TestHeadRevision(t *testing.T) {
runner := &fakeRunner{outputs: []string{"abc123\n"}}
got, err := HeadRevision(runner, "/repo")
if err != nil {
t.Fatalf("HeadRevision: %v", err)
}
if got != "abc123" {
t.Fatalf("revision: got %q", got)
}
}
func TestChangedFiles(t *testing.T) {
runner := &fakeRunner{outputs: []string{"README.md\nservices/core/main.go\n\n"}}
files, err := ChangedFiles(runner, "/repo", "abc", "def")
if err != nil {
t.Fatalf("ChangedFiles: %v", err)
}
want := []string{"README.md", "services/core/main.go"}
if !reflect.DeepEqual(files, want) {
t.Fatalf("files: got %#v want %#v", files, want)
}
}
func TestChangedFilesRequiresRevisions(t *testing.T) {
_, err := ChangedFiles(&fakeRunner{}, "/repo", "", "def")
if !errors.Is(err, ErrInvalidGitInput) {
t.Fatalf("expected ErrInvalidGitInput, got %v", err)
}
}
func TestChangedFilesWithStatus(t *testing.T) {
runner := &fakeRunner{outputs: []string{"A\tREADME.md\nM\tservices/core/main.go\nD\told.md\nR100\tfrom.md\tto.md\n"}}
files, err := ChangedFilesWithStatus(runner, "/repo", "abc", "def")
if err != nil {
t.Fatalf("ChangedFilesWithStatus: %v", err)
}
want := []ChangedFile{
{Path: "README.md", ChangeType: "added"},
{Path: "services/core/main.go", ChangeType: "modified"},
{Path: "old.md", ChangeType: "deleted"},
{Path: "to.md", ChangeType: "renamed"},
}
if !reflect.DeepEqual(files, want) {
t.Fatalf("files:\n got %#v\nwant %#v", files, want)
}
}
func runGit(t *testing.T, dir string, args ...string) {
cmd := exec.Command("git", args...)
cmd.Dir = dir
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git %s failed in %s: %v\nOutput: %s", strings.Join(args, " "), dir, err, string(out))
}
}
func gitOutput(t *testing.T, dir string, args ...string) string {
cmd := exec.Command("git", args...)
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("git %s failed in %s: %v\nOutput: %s", strings.Join(args, " "), dir, err, string(out))
}
return strings.TrimSpace(string(out))
}
func writeFile(t *testing.T, path, content string) {
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("WriteFile %s failed: %v", path, err)
}
}
func setupBareRemote(t *testing.T) (barePath, seedPath string, initialRev string) {
tmpDir := t.TempDir()
// 1. Create bare remote repo
barePath = filepath.Join(tmpDir, "remote.git")
if err := os.MkdirAll(barePath, 0755); err != nil {
t.Fatalf("failed to create bare remote dir: %v", err)
}
runGit(t, barePath, "init", "--bare")
// Make sure default branch is main
runGit(t, barePath, "symbolic-ref", "HEAD", "refs/heads/main")
// 2. Create seed repo to initialize the remote
seedPath = filepath.Join(tmpDir, "seed")
if err := os.MkdirAll(seedPath, 0755); err != nil {
t.Fatalf("failed to create seed dir: %v", err)
}
runGit(t, seedPath, "init")
runGit(t, seedPath, "config", "user.name", "Test User")
runGit(t, seedPath, "config", "user.email", "test@example.com")
runGit(t, seedPath, "config", "commit.gpgsign", "false")
// Create first commit
writeFile(t, filepath.Join(seedPath, "README.md"), "# Seed Repository\n")
runGit(t, seedPath, "add", "README.md")
runGit(t, seedPath, "commit", "-m", "initial commit")
runGit(t, seedPath, "branch", "-M", "main")
// Push to bare remote
runGit(t, seedPath, "remote", "add", "origin", barePath)
runGit(t, seedPath, "push", "-u", "origin", "main")
initialRev = gitOutput(t, seedPath, "rev-parse", "HEAD")
return barePath, seedPath, initialRev
}
func TestCloneFetchWithLocalBareRemote(t *testing.T) {
barePath, seedPath, initialRev := setupBareRemote(t)
tmpDir := t.TempDir()
worktreePath := filepath.Join(tmpDir, "worktree")
// 1. Clone
cli := CLI{}
err := Clone(cli, CloneOptions{
RemoteURL: barePath,
WorktreePath: worktreePath,
Branch: "main",
})
if err != nil {
t.Fatalf("Clone failed: %v", err)
}
// Verify cloned commit
rev, err := HeadRevision(cli, worktreePath)
if err != nil {
t.Fatalf("HeadRevision failed: %v", err)
}
if rev != initialRev {
t.Fatalf("expected cloned rev %q, got %q", initialRev, rev)
}
// 2. Make new commit in seed and push to bare remote
writeFile(t, filepath.Join(seedPath, "file.txt"), "hello")
runGit(t, seedPath, "add", "file.txt")
runGit(t, seedPath, "commit", "-m", "second commit")
runGit(t, seedPath, "push", "origin", "main")
newRev := gitOutput(t, seedPath, "rev-parse", "HEAD")
// 3. Fetch in worktree
err = Fetch(cli, worktreePath, "origin")
if err != nil {
t.Fatalf("Fetch failed: %v", err)
}
// Verify origin/main has the new commit
fetchedRev := gitOutput(t, worktreePath, "rev-parse", "origin/main")
if fetchedRev != newRev {
t.Fatalf("expected fetched rev %q, got %q", newRev, fetchedRev)
}
}
func TestStatusDiffWithCleanStagedUnstagedWorkspace(t *testing.T) {
barePath, _, _ := setupBareRemote(t)
tmpDir := t.TempDir()
worktreePath := filepath.Join(tmpDir, "worktree")
cli := CLI{}
if err := Clone(cli, CloneOptions{RemoteURL: barePath, WorktreePath: worktreePath, Branch: "main"}); err != nil {
t.Fatalf("Clone failed: %v", err)
}
// 1. Clean workspace
status, err := Status(cli, worktreePath)
if err != nil {
t.Fatalf("Status failed: %v", err)
}
if !status.Clean {
t.Fatalf("expected clean status, got porcelain: %q", status.Output)
}
diff, err := Diff(cli, worktreePath, DiffOptions{})
if err != nil {
t.Fatalf("Diff failed: %v", err)
}
if diff != "" {
t.Fatalf("expected empty unstaged diff, got: %q", diff)
}
// 2. Unstaged changes (modified file)
writeFile(t, filepath.Join(worktreePath, "README.md"), "# Seed Repository\nmodified content\n")
status, err = Status(cli, worktreePath)
if err != nil {
t.Fatalf("Status failed: %v", err)
}
if status.Clean {
t.Fatal("expected status to be dirty (unstaged changes)")
}
if !strings.Contains(status.Output, " README.md") {
t.Fatalf("expected README.md in status, got: %q", status.Output)
}
diff, err = Diff(cli, worktreePath, DiffOptions{})
if err != nil {
t.Fatalf("Diff failed: %v", err)
}
if !strings.Contains(diff, "+modified content") {
t.Fatalf("expected diff to show modifications, got: %q", diff)
}
// Staged diff should be empty
stagedDiff, err := Diff(cli, worktreePath, DiffOptions{Staged: true})
if err != nil {
t.Fatalf("Diff staged failed: %v", err)
}
if stagedDiff != "" {
t.Fatalf("expected empty staged diff, got: %q", stagedDiff)
}
// 3. Staged changes
runGit(t, worktreePath, "add", "README.md")
status, err = Status(cli, worktreePath)
if err != nil {
t.Fatalf("Status failed: %v", err)
}
if status.Clean {
t.Fatal("expected status to be dirty (staged changes)")
}
// Unstaged diff should be empty now
diff, err = Diff(cli, worktreePath, DiffOptions{})
if err != nil {
t.Fatalf("Diff failed: %v", err)
}
if diff != "" {
t.Fatalf("expected empty unstaged diff after staging, got: %q", diff)
}
// Staged diff should show modifications
stagedDiff, err = Diff(cli, worktreePath, DiffOptions{Staged: true})
if err != nil {
t.Fatalf("Diff staged failed: %v", err)
}
if !strings.Contains(stagedDiff, "+modified content") {
t.Fatalf("expected staged diff to show modifications, got: %q", stagedDiff)
}
}
func TestCommitPushToLocalBareRemote(t *testing.T) {
barePath, _, _ := setupBareRemote(t)
tmpDir := t.TempDir()
worktreePath := filepath.Join(tmpDir, "worktree")
cli := CLI{}
if err := Clone(cli, CloneOptions{RemoteURL: barePath, WorktreePath: worktreePath, Branch: "main"}); err != nil {
t.Fatalf("Clone failed: %v", err)
}
// Configure local git user for the worktree as well
runGit(t, worktreePath, "config", "user.name", "Test User")
runGit(t, worktreePath, "config", "user.email", "test@example.com")
runGit(t, worktreePath, "config", "commit.gpgsign", "false")
// Make changes and commit
writeFile(t, filepath.Join(worktreePath, "newfile.txt"), "hello commit")
err := Commit(cli, worktreePath, CommitOptions{
Message: "add newfile",
All: true,
})
if err != nil {
t.Fatalf("Commit failed: %v", err)
}
localRev, err := HeadRevision(cli, worktreePath)
if err != nil {
t.Fatalf("HeadRevision failed: %v", err)
}
// Push
err = Push(cli, worktreePath, "origin", "main")
if err != nil {
t.Fatalf("Push failed: %v", err)
}
// Verify bare remote branch has updated to local commit revision
remoteRev := gitOutput(t, barePath, "rev-parse", "main")
if remoteRev != localRev {
t.Fatalf("expected remote revision %q, got %q", localRev, remoteRev)
}
}
func TestCheckoutBranchFromSourceArgs(t *testing.T) {
runner := &fakeRunner{}
// 1. Simple checkout
if err := CheckoutBranch(runner, "/repo", CheckoutOptions{Branch: "main"}); err != nil {
t.Fatalf("CheckoutBranch failed: %v", err)
}
// 2. Checkout with start point
if err := CheckoutBranch(runner, "/repo", CheckoutOptions{Branch: "feature/a", StartPoint: "main"}); err != nil {
t.Fatalf("CheckoutBranch failed: %v", err)
}
// 3. Checkout reset with start point
if err := CheckoutBranch(runner, "/repo", CheckoutOptions{Branch: "feature/b", StartPoint: "main", Reset: true}); err != nil {
t.Fatalf("CheckoutBranch failed: %v", err)
}
want := []fakeCall{
{workdir: "/repo", args: []string{"checkout", "main"}},
{workdir: "/repo", args: []string{"checkout", "-b", "feature/a", "main"}},
{workdir: "/repo", args: []string{"checkout", "-B", "feature/b", "main"}},
}
if !reflect.DeepEqual(runner.calls, want) {
t.Fatalf("calls:\n got %#v\nwant %#v", runner.calls, want)
}
}
func TestCheckoutBranchFromSource(t *testing.T) {
barePath, _, initialRev := setupBareRemote(t)
tmpDir := t.TempDir()
worktreePath := filepath.Join(tmpDir, "worktree")
cli := CLI{}
if err := Clone(cli, CloneOptions{RemoteURL: barePath, WorktreePath: worktreePath, Branch: "main"}); err != nil {
t.Fatalf("Clone failed: %v", err)
}
// Configure local user
runGit(t, worktreePath, "config", "user.name", "Test User")
runGit(t, worktreePath, "config", "user.email", "test@example.com")
runGit(t, worktreePath, "config", "commit.gpgsign", "false")
// Make another commit in main
writeFile(t, filepath.Join(worktreePath, "main_only.txt"), "main data")
if err := Commit(cli, worktreePath, CommitOptions{Message: "commit on main", All: true}); err != nil {
t.Fatalf("Commit failed: %v", err)
}
mainRev, err := HeadRevision(cli, worktreePath)
if err != nil {
t.Fatalf("HeadRevision failed: %v", err)
}
// 1. Checkout new branch from main's initialRev (start point)
err = CheckoutBranch(cli, worktreePath, CheckoutOptions{
Branch: "feature/from-start",
StartPoint: initialRev,
})
if err != nil {
t.Fatalf("CheckoutBranch from initialRev failed: %v", err)
}
// Verify we are on feature/from-start, and revision is initialRev (not mainRev)
currentRev, err := HeadRevision(cli, worktreePath)
if err != nil {
t.Fatalf("HeadRevision failed: %v", err)
}
if currentRev != initialRev {
t.Fatalf("expected HEAD revision to be %q, got %q", initialRev, currentRev)
}
// 2. Checkout new branch from main's current mainRev (start point) with Reset: true
// To test Reset flag (-B), checkout main again
if err := Checkout(cli, worktreePath, "main"); err != nil {
t.Fatalf("Checkout main failed: %v", err)
}
// Create branch feature/reset with initialRev first
err = CheckoutBranch(cli, worktreePath, CheckoutOptions{
Branch: "feature/reset",
StartPoint: initialRev,
})
if err != nil {
t.Fatalf("CheckoutBranch feature/reset failed: %v", err)
}
// Now reset it to mainRev
err = CheckoutBranch(cli, worktreePath, CheckoutOptions{
Branch: "feature/reset",
StartPoint: mainRev,
Reset: true,
})
if err != nil {
t.Fatalf("CheckoutBranch reset failed: %v", err)
}
currentRev, err = HeadRevision(cli, worktreePath)
if err != nil {
t.Fatalf("HeadRevision failed: %v", err)
}
if currentRev != mainRev {
t.Fatalf("expected HEAD revision to be reset to %q, got %q", mainRev, currentRev)
}
}