186 lines
9.1 KiB
Go
186 lines
9.1 KiB
Go
package backendapp
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/kandev/kandev/internal/agent/runtime/lifecycle"
|
|
githubpkg "github.com/kandev/kandev/internal/github"
|
|
executorpkg "github.com/kandev/kandev/internal/orchestrator/executor"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
"github.com/kandev/kandev/internal/worktree"
|
|
)
|
|
|
|
func TestForkPRBasePreparationEndToEnd(t *testing.T) {
|
|
root := t.TempDir()
|
|
gitConfig := filepath.Join(root, "empty-git-config")
|
|
if err := os.WriteFile(gitConfig, nil, 0o600); err != nil {
|
|
t.Fatalf("create isolated Git config: %v", err)
|
|
}
|
|
t.Setenv("GIT_CONFIG_GLOBAL", gitConfig)
|
|
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")
|
|
repoPath, upstreamBaseOID, prHeadOID, forkMainOID, forkTargetOID := initForkPRLaunchFixture(t, root)
|
|
|
|
providerPR := &githubpkg.PR{
|
|
Number: 42, RepoOwner: "upstream", RepoName: "widget", BaseRepoOwner: "upstream", BaseRepoName: "widget",
|
|
HeadRepoOwner: "fork-owner", HeadRepoName: "widget", HeadBranch: "feature/pr-42",
|
|
BaseBranch: "release/next", BaseSHA: upstreamBaseOID,
|
|
}
|
|
base, err := githubPRBaseFromPR(providerPR, "upstream", "widget", 42, "feature/pr-42")
|
|
if err != nil {
|
|
t.Fatalf("convert provider PR response: %v", err)
|
|
}
|
|
if base.OID != upstreamBaseOID {
|
|
t.Fatalf("provider target OID = %q, want upstream %q", base.OID, upstreamBaseOID)
|
|
}
|
|
|
|
taskRepositoryMetadata := map[string]interface{}{}
|
|
if err := models.PutComparisonTarget(taskRepositoryMetadata, &base.Target); err != nil {
|
|
t.Fatalf("store task comparison target: %v", err)
|
|
}
|
|
storedTarget, found, err := models.LoadComparisonTarget(taskRepositoryMetadata)
|
|
if err != nil || !found || !storedTarget.Equal(base.Target) {
|
|
t.Fatalf("stored comparison target = %#v, found=%v, err=%v", storedTarget, found, err)
|
|
}
|
|
|
|
launchRequest := buildLifecycleLaunchRequest(&executorpkg.LaunchAgentRequest{
|
|
TaskID: "task-fork-pr", WorkspaceID: "workspace-fork-pr", SessionID: "session-fork-pr",
|
|
TaskDirName: "task-fork-pr", RepoName: "widget", UseWorktree: true,
|
|
Repositories: []executorpkg.RepoSpec{{
|
|
TaskRepositoryID: "task-repo-fork-pr", RepositoryID: "repo-fork-pr", RepositoryPath: repoPath,
|
|
RepoName: "widget", BaseBranch: base.Target.TargetBranch, DefaultBranch: "main",
|
|
CheckoutBranch: base.Target.HeadBranch, PRNumber: base.Target.Number,
|
|
ComparisonTarget: &base.Target, QualifiedPRBase: &base, RemoteSyncHandled: true,
|
|
}},
|
|
}, filepath.Join(root, "workspace"), "")
|
|
if len(launchRequest.Repositories) != 1 {
|
|
t.Fatalf("lifecycle launch repository count = %d, want 1", len(launchRequest.Repositories))
|
|
}
|
|
repoSpec := launchRequest.Repositories[0]
|
|
if repoSpec.QualifiedPRBase == nil || !reflect.DeepEqual(*repoSpec.QualifiedPRBase, base) ||
|
|
repoSpec.ComparisonTarget == nil || !repoSpec.ComparisonTarget.Equal(base.Target) || repoSpec.PRNumber != 42 {
|
|
t.Fatalf("lifecycle launch repository lost provider identity: %#v", repoSpec)
|
|
}
|
|
|
|
worktreeConfig := worktree.Config{
|
|
Enabled: true, TasksBasePath: filepath.Join(root, "tasks"), BranchPrefix: "kandev/",
|
|
}
|
|
mgr, err := worktree.NewManager(worktreeConfig, nil, newTestLogger())
|
|
if err != nil {
|
|
t.Fatalf("create worktree manager: %v", err)
|
|
}
|
|
preparer := lifecycle.NewWorktreePreparer(mgr, newTestLogger())
|
|
prepared, err := preparer.Prepare(context.Background(), &lifecycle.EnvPrepareRequest{
|
|
TaskID: launchRequest.TaskID, WorkspaceID: launchRequest.WorkspaceID, SessionID: launchRequest.SessionID,
|
|
RepositoryID: repoSpec.RepositoryID, TaskRepositoryID: repoSpec.TaskRepositoryID,
|
|
RepositoryPath: repoSpec.RepositoryPath, UseWorktree: true,
|
|
TaskDirName: launchRequest.TaskDirName, RepoName: repoSpec.RepoName,
|
|
BaseBranch: repoSpec.BaseBranch, DefaultBranch: repoSpec.DefaultBranch,
|
|
CheckoutBranch: repoSpec.CheckoutBranch, PRNumber: repoSpec.PRNumber,
|
|
QualifiedPRBase: repoSpec.QualifiedPRBase,
|
|
RemoteSyncHandled: repoSpec.RemoteSyncHandled,
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatalf("prepare fork PR worktree: %v", err)
|
|
}
|
|
if !prepared.Success {
|
|
t.Fatalf("fork PR preparation failed: %s", prepared.ErrorMessage)
|
|
}
|
|
if prepared.BaseBranch != "release/next" || prepared.RequestedBaseBranch != "release/next" {
|
|
t.Fatalf("prepared base = %q (requested %q), want release/next", prepared.BaseBranch, prepared.RequestedBaseBranch)
|
|
}
|
|
if got := strings.TrimSpace(runGit(t, prepared.WorkspacePath, "rev-parse", "HEAD")); got != prHeadOID {
|
|
t.Fatalf("prepared HEAD = %q, want PR head %q", got, prHeadOID)
|
|
}
|
|
if got := strings.TrimSpace(runGit(t, repoPath, "rev-parse", base.Target.ComparisonRef()+"^{commit}")); got != upstreamBaseOID {
|
|
t.Fatalf("qualified target ref = %q, want upstream OID %q", got, upstreamBaseOID)
|
|
}
|
|
if got := strings.TrimSpace(runGit(t, repoPath, "rev-parse", "refs/remotes/origin/main")); got != forkMainOID {
|
|
t.Fatalf("origin/main = %q, want unchanged fork OID %q", got, forkMainOID)
|
|
}
|
|
if got := strings.TrimSpace(runGit(t, repoPath, "rev-parse", "refs/remotes/origin/release/next")); got != forkTargetOID {
|
|
t.Fatalf("origin/release/next = %q, want unchanged fork OID %q", got, forkTargetOID)
|
|
}
|
|
if got := strings.TrimSpace(runGit(t, repoPath, "config", "--get", "remote.origin.url")); got != filepath.Join(root, "fork.git") {
|
|
t.Fatalf("origin URL = %q, want fork remote", got)
|
|
}
|
|
if got := strings.TrimSpace(runGit(t, repoPath, "config", "--get", "remote.origin.pushurl")); got != "https://push.example/fork/widget.git" {
|
|
t.Fatalf("origin push URL = %q, want unchanged fork push route", got)
|
|
}
|
|
if got := strings.TrimSpace(runGit(t, repoPath, "config", "--get", "remote."+base.Target.ComparisonRemoteName()+".pushurl")); got != "DISABLED" {
|
|
t.Fatalf("comparison remote push URL = %q, want disabled", got)
|
|
}
|
|
finalTarget, found, err := models.LoadComparisonTarget(taskRepositoryMetadata)
|
|
if err != nil || !found || !finalTarget.Equal(base.Target) {
|
|
t.Fatalf("task comparison metadata changed during preparation: target=%#v found=%v err=%v", finalTarget, found, err)
|
|
}
|
|
}
|
|
|
|
func initForkPRLaunchFixture(t *testing.T, root string) (repoPath, upstreamBaseOID, prHeadOID, forkMainOID, forkTargetOID string) {
|
|
t.Helper()
|
|
upstreamBare := filepath.Join(root, "upstream.git")
|
|
forkBare := filepath.Join(root, "fork.git")
|
|
upstreamSeed := filepath.Join(root, "upstream-seed")
|
|
forkSeed := filepath.Join(root, "fork-seed")
|
|
runGit(t, root, "init", "--bare", "-b", "main", upstreamBare)
|
|
runGit(t, root, "init", "-b", "main", upstreamSeed)
|
|
configureForkPRLaunchCommitter(t, upstreamSeed)
|
|
writeForkPRLaunchFile(t, upstreamSeed, "base.txt", "upstream main\n")
|
|
runGit(t, upstreamSeed, "add", "base.txt")
|
|
runGit(t, upstreamSeed, "commit", "-m", "upstream main")
|
|
runGit(t, upstreamSeed, "remote", "add", "origin", upstreamBare)
|
|
runGit(t, upstreamSeed, "push", "origin", "main")
|
|
runGit(t, upstreamSeed, "checkout", "-b", "release/next")
|
|
writeForkPRLaunchFile(t, upstreamSeed, "target.txt", "upstream release\n")
|
|
runGit(t, upstreamSeed, "add", "target.txt")
|
|
runGit(t, upstreamSeed, "commit", "-m", "upstream release")
|
|
runGit(t, upstreamSeed, "push", "origin", "release/next")
|
|
upstreamBaseOID = strings.TrimSpace(runGit(t, upstreamSeed, "rev-parse", "HEAD"))
|
|
runGit(t, upstreamSeed, "checkout", "-b", "feature/pr-42")
|
|
writeForkPRLaunchFile(t, upstreamSeed, "pr.txt", "pull request head\n")
|
|
runGit(t, upstreamSeed, "add", "pr.txt")
|
|
runGit(t, upstreamSeed, "commit", "-m", "pull request head")
|
|
prHeadOID = strings.TrimSpace(runGit(t, upstreamSeed, "rev-parse", "HEAD"))
|
|
runGit(t, upstreamSeed, "push", "origin", "HEAD:refs/pull/42/head")
|
|
|
|
runGit(t, root, "init", "--bare", "-b", "main", forkBare)
|
|
runGit(t, root, "init", "-b", "main", forkSeed)
|
|
configureForkPRLaunchCommitter(t, forkSeed)
|
|
writeForkPRLaunchFile(t, forkSeed, "base.txt", "fork main\n")
|
|
runGit(t, forkSeed, "add", "base.txt")
|
|
runGit(t, forkSeed, "commit", "-m", "fork main")
|
|
runGit(t, forkSeed, "remote", "add", "origin", forkBare)
|
|
runGit(t, forkSeed, "push", "origin", "main")
|
|
forkMainOID = strings.TrimSpace(runGit(t, forkSeed, "rev-parse", "HEAD"))
|
|
runGit(t, forkSeed, "checkout", "-b", "release/next")
|
|
writeForkPRLaunchFile(t, forkSeed, "target.txt", "fork release\n")
|
|
runGit(t, forkSeed, "add", "target.txt")
|
|
runGit(t, forkSeed, "commit", "-m", "fork release")
|
|
runGit(t, forkSeed, "push", "origin", "release/next")
|
|
forkTargetOID = strings.TrimSpace(runGit(t, forkSeed, "rev-parse", "HEAD"))
|
|
|
|
repoPath = filepath.Join(root, "fork-checkout")
|
|
runGit(t, root, "clone", forkBare, repoPath)
|
|
runGit(t, repoPath, "config", "url.file://"+upstreamBare+".insteadOf", "https://github.com/upstream/widget.git")
|
|
runGit(t, repoPath, "config", "remote.origin.pushurl", "https://push.example/fork/widget.git")
|
|
return repoPath, upstreamBaseOID, prHeadOID, forkMainOID, forkTargetOID
|
|
}
|
|
|
|
func configureForkPRLaunchCommitter(t *testing.T, repoPath string) {
|
|
t.Helper()
|
|
runGit(t, repoPath, "config", "user.email", "test@example.com")
|
|
runGit(t, repoPath, "config", "user.name", "Test User")
|
|
runGit(t, repoPath, "config", "commit.gpgsign", "false")
|
|
}
|
|
|
|
func writeForkPRLaunchFile(t *testing.T, repoPath, name, contents string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(filepath.Join(repoPath, name), []byte(contents), 0o600); err != nil {
|
|
t.Fatalf("write %s: %v", name, err)
|
|
}
|
|
}
|