From c1c1ff7fe2725fe3cd183abfdfc40eef883f929a Mon Sep 17 00:00:00 2001 From: Aditi Chauhan Date: Wed, 1 Jul 2026 15:12:25 +0530 Subject: [PATCH] fix: detect workspace root default branch --- .../workspace/gitworktree/workspace_test.go | 77 +++++++++++++++++++ backend/internal/service/project/service.go | 5 ++ .../internal/service/project/service_test.go | 24 ++++++ .../internal/session_manager/manager_test.go | 39 ++++++++++ 4 files changed, 145 insertions(+) diff --git a/backend/internal/adapters/workspace/gitworktree/workspace_test.go b/backend/internal/adapters/workspace/gitworktree/workspace_test.go index cf676ee11..5cfb5c07e 100644 --- a/backend/internal/adapters/workspace/gitworktree/workspace_test.go +++ b/backend/internal/adapters/workspace/gitworktree/workspace_test.go @@ -174,6 +174,60 @@ func TestOrchestratorManagedPath(t *testing.T) { }) } +func TestCreateWorkspaceProjectOrchestratorCreatesRootAndChildWorktrees(t *testing.T) { + root := t.TempDir() + parentRepo := gitRepoWithInitialCommit(t, filepath.Join(t.TempDir(), "workspace-root"), "main") + childRepo := gitRepoWithInitialCommit(t, filepath.Join(t.TempDir(), "api"), "main") + ws, err := New(Options{ManagedRoot: root, RepoResolver: StaticRepoResolver{"proj": parentRepo}}) + if err != nil { + t.Fatalf("new: %v", err) + } + + info, err := ws.CreateWorkspaceProject(context.Background(), ports.WorkspaceProjectConfig{ + ProjectID: "proj", + SessionID: "proj-1", + Kind: domain.KindOrchestrator, + SessionPrefix: "proj", + Branch: "ao/proj-1", + RootRepoPath: parentRepo, + BaseBranch: "main", + Repos: []ports.WorkspaceProjectRepoConfig{{ + Name: "api", + RelativePath: "api", + RepoPath: childRepo, + BaseBranch: "main", + }}, + }) + if err != nil { + t.Fatalf("CreateWorkspaceProject: %v", err) + } + wantRoot := filepath.Join(ws.managedRoot, "proj", "orchestrator", "proj-orchestrator") + wantChild := filepath.Join(wantRoot, "api") + wantChildRepo, err := physicalAbs(childRepo) + if err != nil { + t.Fatalf("child physical path: %v", err) + } + if info.Root.Path != wantRoot || info.Root.Branch != "ao/proj-1" { + t.Fatalf("root = %#v, want path %q branch ao/proj-1", info.Root, wantRoot) + } + if len(info.Worktrees) != 2 { + t.Fatalf("worktrees = %d, want root and api: %#v", len(info.Worktrees), info.Worktrees) + } + if info.Worktrees[1].Path != wantChild || info.Worktrees[1].RepoPath != wantChildRepo { + t.Fatalf("child worktree = %#v, want path %q repo %q", info.Worktrees[1], wantChild, wantChildRepo) + } + if out, err := exec.Command("git", "-C", parentRepo, "worktree", "list", "--porcelain").CombinedOutput(); err != nil { + t.Fatalf("root worktree list: %v (%s)", err, out) + } else if !strings.Contains(string(out), wantRoot) { + t.Fatalf("root worktree list missing %q:\n%s", wantRoot, out) + } + if out, err := exec.Command("git", "-C", childRepo, "worktree", "list", "--porcelain").CombinedOutput(); err != nil { + t.Fatalf("child worktree list: %v (%s)", err, out) + } else if !strings.Contains(string(out), wantChild) { + t.Fatalf("child worktree list missing %q:\n%s", wantChild, out) + } +} + func TestCreateReusesRegisteredWorktreeAtExpectedPath(t *testing.T) { root := t.TempDir() repo := t.TempDir() @@ -532,3 +586,26 @@ func mkdirFile(dir, name string) error { } return os.WriteFile(filepath.Join(dir, name), []byte("data"), 0o644) } + +func gitRepoWithInitialCommit(t *testing.T, dir, branch string) string { + t.Helper() + if out, err := exec.Command("git", "init", "-b", branch, dir).CombinedOutput(); err != nil { + t.Fatalf("git init: %v (%s)", err, out) + } + if out, err := exec.Command("git", "-C", dir, "config", "user.email", "ao@example.com").CombinedOutput(); err != nil { + t.Fatalf("git config email: %v (%s)", err, out) + } + if out, err := exec.Command("git", "-C", dir, "config", "user.name", "AO Test").CombinedOutput(); err != nil { + t.Fatalf("git config name: %v (%s)", err, out) + } + if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("hello\n"), 0o644); err != nil { + t.Fatalf("write readme: %v", err) + } + if out, err := exec.Command("git", "-C", dir, "add", "README.md").CombinedOutput(); err != nil { + t.Fatalf("git add: %v (%s)", err, out) + } + if out, err := exec.Command("git", "-C", dir, "commit", "-m", "initial").CombinedOutput(); err != nil { + t.Fatalf("git commit: %v (%s)", err, out) + } + return dir +} diff --git a/backend/internal/service/project/service.go b/backend/internal/service/project/service.go index af8593737..63f32bf42 100644 --- a/backend/internal/service/project/service.go +++ b/backend/internal/service/project/service.go @@ -196,6 +196,11 @@ func (m *Service) Add(ctx context.Context, in AddInput) (Project, error) { if err != nil { return Project{}, err } + if row.Config.DefaultBranch == "" { + if branch := resolveDefaultBranch(path); branch != "" && branch != domain.DefaultBranchName { + row.Config.DefaultBranch = branch + } + } row.Kind = domain.ProjectKindWorkspace row.RepoOriginURL = resolveGitOriginURL(path) if err := m.store.UpsertWorkspaceProject(ctx, row, repos); err != nil { diff --git a/backend/internal/service/project/service_test.go b/backend/internal/service/project/service_test.go index 9cd9d219d..1643cadfd 100644 --- a/backend/internal/service/project/service_test.go +++ b/backend/internal/service/project/service_test.go @@ -634,6 +634,30 @@ func TestManager_AddWorkspaceDetectsChildDefaultBranches(t *testing.T) { } } +func TestManager_AddWorkspaceDetectsParentDefaultBranch(t *testing.T) { + configureCommitter(t) + ctx := context.Background() + m := newManager(t) + parent := t.TempDir() + gitRepoWithCommitOnBranch(t, parent, "master") + gitRepoWithCommit(t, filepath.Join(parent, "api")) + + proj, err := m.Add(ctx, project.AddInput{Path: parent, ProjectID: ptr("ws"), AsWorkspace: true}) + if err != nil { + t.Fatalf("Add workspace: %v", err) + } + if proj.DefaultBranch != "master" { + t.Fatalf("workspace parent default branch = %q, want master", proj.DefaultBranch) + } + got, err := m.Get(ctx, "ws") + if err != nil { + t.Fatalf("Get workspace: %v", err) + } + if got.Project == nil || got.Project.DefaultBranch != "master" { + t.Fatalf("stored workspace default branch = %#v, want master", got.Project) + } +} + func TestManager_AddWorkspaceRejectsUncommittedChild(t *testing.T) { configureCommitter(t) ctx := context.Background() diff --git a/backend/internal/session_manager/manager_test.go b/backend/internal/session_manager/manager_test.go index 7dfc22ebc..81dec6b9e 100644 --- a/backend/internal/session_manager/manager_test.go +++ b/backend/internal/session_manager/manager_test.go @@ -679,6 +679,45 @@ func TestSpawn_WorkspaceProjectRecordsRootAndChildWorktrees(t *testing.T) { } } +func TestSpawn_WorkspaceProjectOrchestratorUsesOrchestratorPath(t *testing.T) { + st := newFakeStore() + st.projects["mer"] = domain.ProjectRecord{ + ID: "mer", + Path: "/repo/mer", + Kind: domain.ProjectKindWorkspace, + Config: testRoleAgents(), + } + st.workspaceRepo["mer"] = []domain.WorkspaceRepoRecord{{Name: "api", RelativePath: "api", DefaultBranch: "main"}} + rt := &fakeRuntime{} + ws := &fakeWorkspace{path: "/managed/mer/orchestrator/mer-orchestrator"} + m := New(Deps{ + Runtime: rt, Agents: fakeAgents{}, Workspace: ws, Store: st, + Messenger: &fakeMessenger{}, Lifecycle: &fakeLCM{store: st}, + LookPath: func(string) (string, error) { return "/bin/true", nil }, + }) + + rec, err := m.Spawn(ctx, ports.SpawnConfig{ProjectID: "mer", Kind: domain.KindOrchestrator}) + if err != nil { + t.Fatal(err) + } + if rec.Kind != domain.KindOrchestrator { + t.Fatalf("kind = %q, want orchestrator", rec.Kind) + } + if rec.Metadata.WorkspacePath != "/managed/mer/orchestrator/mer-orchestrator" { + t.Fatalf("workspace path = %q", rec.Metadata.WorkspacePath) + } + if ws.lastProjectCfg.Kind != domain.KindOrchestrator { + t.Fatalf("workspace project kind = %q, want orchestrator", ws.lastProjectCfg.Kind) + } + if ws.lastProjectCfg.Branch != "ao/mer-1" { + t.Fatalf("workspace project branch = %q, want ao/mer-1", ws.lastProjectCfg.Branch) + } + rows := st.worktrees["mer-1"] + if len(rows) != 2 { + t.Fatalf("session worktree rows = %d, want root and api: %#v", len(rows), rows) + } +} + func TestSpawn_WorkspaceProjectRollsBackAllWorktreesOnRuntimeFailure(t *testing.T) { m, st, _, ws := newManager() st.projects["mer"] = domain.ProjectRecord{