Files
kandev/apps/backend/internal/task/service/service_branch_update_test.go
T

555 lines
21 KiB
Go

package service
import (
"context"
"errors"
"sync"
"testing"
"github.com/kandev/kandev/internal/events"
"github.com/kandev/kandev/internal/task/models"
wfmodels "github.com/kandev/kandev/internal/workflow/models"
)
func eventBusHasType(bus *MockEventBus, eventType string) bool {
for _, e := range bus.GetPublishedEvents() {
if e.Type == eventType {
return true
}
}
return false
}
// fakeBaseBranchPusher records calls so tests can assert the service
// invoked the live agentctl push with the right per-repo map.
type fakeBaseBranchPusher struct {
mu sync.Mutex
calls []fakeBaseBranchPusherCall
}
type fakeBaseBranchPusherCall struct {
taskID string
branches map[string]string
}
func (f *fakeBaseBranchPusher) PushBaseBranchesForTask(_ context.Context, taskID string, branches map[string]string) {
f.mu.Lock()
defer f.mu.Unlock()
f.calls = append(f.calls, fakeBaseBranchPusherCall{taskID: taskID, branches: branches})
}
func (f *fakeBaseBranchPusher) snapshot() []fakeBaseBranchPusherCall {
f.mu.Lock()
defer f.mu.Unlock()
out := make([]fakeBaseBranchPusherCall, len(f.calls))
copy(out, f.calls)
return out
}
func setBranchUpdateWorkflowStep(svc *Service) {
svc.SetWorkflowStepGetter(&fakeWorkflowStepGetter{steps: map[string]*wfmodels.WorkflowStep{
"step-1": {ID: "step-1", WorkflowID: "wf-1", Name: "Step"},
}})
}
// TestUpdateRepositoryBaseBranch_ResetsSessionBases confirms the picker
// path also clears session.base_commit_sha and rewrites session.base_branch
// for affected (task, repo) pairs. Without this the commits panel and
// cumulative diff stay filtered against the captured-at-launch SHA and the
// user sees "commits disappeared" after switching the base.
func TestUpdateRepositoryBaseBranch_ResetsSessionBases(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
setBranchUpdateWorkflowStep(svc)
svc.SetAgentBaseBranchPusher(&fakeBaseBranchPusher{})
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "WS"})
_ = repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-1", WorkspaceID: "ws-1", Name: "WF"})
_ = repo.CreateRepository(ctx, &models.Repository{ID: "repo-1", WorkspaceID: "ws-1", Name: "frontend", DefaultBranch: "main"})
taskResult, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1", WorkflowID: "wf-1", WorkflowStepID: "step-1", Title: "Sessions",
Repositories: []TaskRepositoryInput{{RepositoryID: "repo-1", BaseBranch: "main", CheckoutBranch: "feature/x"}},
})
task := taskResult.Task
if err != nil {
t.Fatalf("CreateTask: %v", err)
}
rows, _ := repo.ListTaskRepositories(ctx, task.ID)
taskRepoID := rows[0].ID
sess := &models.TaskSession{
ID: "sess-1", TaskID: task.ID, RepositoryID: "repo-1",
BaseBranch: "main", BaseCommitSHA: "captured-old-sha",
}
if err := repo.CreateTaskSession(ctx, sess); err != nil {
t.Fatalf("CreateTaskSession: %v", err)
}
if _, err := svc.UpdateRepositoryBaseBranch(ctx, UpdateRepositoryBaseBranchRequest{
TaskID: task.ID, TaskRepositoryID: taskRepoID, BaseBranch: "staging",
}); err != nil {
t.Fatalf("UpdateRepositoryBaseBranch: %v", err)
}
reread, err := repo.GetTaskSession(ctx, "sess-1")
if err != nil {
t.Fatalf("GetTaskSession: %v", err)
}
if reread.BaseBranch != "staging" {
t.Errorf("session BaseBranch = %q, want staging", reread.BaseBranch)
}
if reread.BaseCommitSHA != "" {
t.Errorf("session BaseCommitSHA = %q, want empty (cleared)", reread.BaseCommitSHA)
}
}
// TestUpdateRepositoryBaseBranch_PersistsAndPushes covers the happy path:
// DB write, task.updated event, and a live agentctl push containing the
// expected per-repo map keyed by Repository.Name.
func TestUpdateRepositoryBaseBranch_PersistsAndPushes(t *testing.T) {
svc, bus, repo := createTestService(t)
ctx := context.Background()
setBranchUpdateWorkflowStep(svc)
pusher := &fakeBaseBranchPusher{}
svc.SetAgentBaseBranchPusher(pusher)
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "WS"})
_ = repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-1", WorkspaceID: "ws-1", Name: "WF"})
_ = repo.CreateRepository(ctx, &models.Repository{ID: "repo-1", WorkspaceID: "ws-1", Name: "frontend", DefaultBranch: "main"})
taskResult, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
WorkflowID: "wf-1",
WorkflowStepID: "step-1",
Title: "Promotion chain",
Repositories: []TaskRepositoryInput{
{RepositoryID: "repo-1", BaseBranch: "main", CheckoutBranch: "feature/a"},
},
})
task := taskResult.Task
if err != nil {
t.Fatalf("CreateTask: %v", err)
}
rows, err := repo.ListTaskRepositories(ctx, task.ID)
if err != nil || len(rows) != 1 {
t.Fatalf("ListTaskRepositories: %v, rows=%d", err, len(rows))
}
taskRepoID := rows[0].ID
bus.ClearEvents()
updated, err := svc.UpdateRepositoryBaseBranch(ctx, UpdateRepositoryBaseBranchRequest{
TaskID: task.ID,
TaskRepositoryID: taskRepoID,
BaseBranch: "staging",
})
if err != nil {
t.Fatalf("UpdateRepositoryBaseBranch: %v", err)
}
if updated.BaseBranch != "staging" {
t.Errorf("returned BaseBranch = %q, want staging", updated.BaseBranch)
}
reread, err := repo.GetTaskRepository(ctx, taskRepoID)
if err != nil {
t.Fatalf("GetTaskRepository after update: %v", err)
}
if reread.BaseBranch != "staging" {
t.Errorf("DB BaseBranch = %q, want staging", reread.BaseBranch)
}
if !eventBusHasType(bus, events.TaskUpdated) {
t.Error("expected task.updated event")
}
calls := pusher.snapshot()
if len(calls) != 1 {
t.Fatalf("expected 1 pusher call, got %d", len(calls))
}
if calls[0].taskID != task.ID {
t.Errorf("pusher call taskID = %q, want %q", calls[0].taskID, task.ID)
}
// Single-repo legacy fallback: the map must carry the empty-key entry
// AND the named entry so both root and per-repo trackers find a value.
if got := calls[0].branches["frontend"]; got != "staging" {
t.Errorf("pusher branches[frontend] = %q, want staging", got)
}
if got := calls[0].branches[""]; got != "staging" {
t.Errorf("pusher branches[\"\"] = %q, want staging (single-repo fallback)", got)
}
}
// TestUpdateRepositoryBaseBranch_NoChangeSkipsWork is a sanity check: when
// the new value equals the stored value, the service short-circuits before
// the DB write so callers don't trigger spurious task.updated events or
// agentctl refreshes.
func TestUpdateRepositoryBaseBranch_NoChangeSkipsWork(t *testing.T) {
svc, bus, repo := createTestService(t)
ctx := context.Background()
setBranchUpdateWorkflowStep(svc)
pusher := &fakeBaseBranchPusher{}
svc.SetAgentBaseBranchPusher(pusher)
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "WS"})
_ = repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-1", WorkspaceID: "ws-1", Name: "WF"})
_ = repo.CreateRepository(ctx, &models.Repository{ID: "repo-1", WorkspaceID: "ws-1", Name: "frontend", DefaultBranch: "main"})
taskResult, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
WorkflowID: "wf-1",
WorkflowStepID: "step-1",
Title: "No-op",
Repositories: []TaskRepositoryInput{
{RepositoryID: "repo-1", BaseBranch: "main", CheckoutBranch: "feature/a"},
},
})
task := taskResult.Task
if err != nil {
t.Fatalf("CreateTask: %v", err)
}
rows, _ := repo.ListTaskRepositories(ctx, task.ID)
bus.ClearEvents()
_, err = svc.UpdateRepositoryBaseBranch(ctx, UpdateRepositoryBaseBranchRequest{
TaskID: task.ID,
TaskRepositoryID: rows[0].ID,
BaseBranch: "main", // identical to stored value
})
if err != nil {
t.Fatalf("UpdateRepositoryBaseBranch: %v", err)
}
if eventBusHasType(bus, events.TaskUpdated) {
t.Error("identical update should not emit task.updated")
}
if len(pusher.snapshot()) != 0 {
t.Error("identical update should not invoke pusher")
}
}
func TestUpdateRepositoryBaseBranch_SameBranchClearsComparisonTarget(t *testing.T) {
svc, bus, repo := createTestService(t)
ctx := context.Background()
setBranchUpdateWorkflowStep(svc)
pusher := &fakeBaseBranchPusher{}
svc.SetAgentBaseBranchPusher(pusher)
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "WS"})
_ = repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-1", WorkspaceID: "ws-1", Name: "WF"})
_ = repo.CreateRepository(ctx, &models.Repository{ID: "repo-1", WorkspaceID: "ws-1", Name: "frontend", DefaultBranch: "main"})
taskResult, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1", WorkflowID: "wf-1", WorkflowStepID: "step-1", Title: "Clear target",
Repositories: []TaskRepositoryInput{{RepositoryID: "repo-1", BaseBranch: "main", CheckoutBranch: "feature/a"}},
})
if err != nil {
t.Fatalf("CreateTask: %v", err)
}
rows, err := repo.ListTaskRepositories(ctx, taskResult.Task.ID)
if err != nil || len(rows) != 1 {
t.Fatalf("ListTaskRepositories: %v, rows=%d", err, len(rows))
}
target := models.ComparisonTarget{
Version: models.ComparisonTargetVersion, Provider: models.ComparisonTargetProviderGitHub,
Kind: models.ComparisonTargetKindPullRequest, Number: 42, HeadBranch: "feature/a", TargetBranch: "main",
HeadRepository: models.ComparisonTargetRepository{Host: "github.com", Path: "contributor/frontend", RemoteURL: "https://github.com/contributor/frontend.git"},
TargetRepository: models.ComparisonTargetRepository{Host: "github.com", Path: "upstream/frontend", RemoteURL: "https://github.com/upstream/frontend.git"},
}
if _, changed, err := repo.UpdateTaskRepositoryComparisonTarget(ctx, rows[0].ID, &target, nil); err != nil || !changed {
t.Fatalf("seed comparison target: changed=%v err=%v", changed, err)
}
bus.ClearEvents()
updated, err := svc.UpdateRepositoryBaseBranch(ctx, UpdateRepositoryBaseBranchRequest{
TaskID: taskResult.Task.ID, TaskRepositoryID: rows[0].ID, BaseBranch: "main",
})
if err != nil {
t.Fatalf("UpdateRepositoryBaseBranch: %v", err)
}
if updated.BaseBranch != "main" {
t.Fatalf("BaseBranch = %q, want main", updated.BaseBranch)
}
if _, ok, err := models.LoadComparisonTarget(updated.Metadata); err != nil || ok {
t.Fatalf("comparison target remains: present=%v err=%v", ok, err)
}
if !eventBusHasType(bus, events.TaskUpdated) {
t.Fatal("same-branch target removal should publish task.updated")
}
if len(pusher.snapshot()) != 1 {
t.Fatalf("same-branch target removal should push live base map once, got %d", len(pusher.snapshot()))
}
}
// TestUpdateRepositoryBaseBranch_RejectsUnsafeRefs ensures unsafe ref
// names (leading "-", shell metacharacters, …) are rejected at the
// service boundary before reaching the DB or the live agentctl push. The
// picker payload is user-controlled and ultimately interpolated into a
// `git` argument list inside agentctl — letting through "-upload-pack="
// would risk command-flag injection.
func TestUpdateRepositoryBaseBranch_RejectsUnsafeRefs(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
setBranchUpdateWorkflowStep(svc)
pusher := &fakeBaseBranchPusher{}
svc.SetAgentBaseBranchPusher(pusher)
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "WS"})
_ = repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-1", WorkspaceID: "ws-1", Name: "WF"})
_ = repo.CreateRepository(ctx, &models.Repository{ID: "repo-1", WorkspaceID: "ws-1", Name: "frontend", DefaultBranch: "main"})
taskResult, _ := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1", WorkflowID: "wf-1", WorkflowStepID: "step-1", Title: "T",
Repositories: []TaskRepositoryInput{{RepositoryID: "repo-1", BaseBranch: "main"}},
})
task := taskResult.Task
rows, _ := repo.ListTaskRepositories(ctx, task.ID)
for _, bad := range []string{"-upload-pack=evil", "main;rm -rf", "branch with space", "/leading-slash"} {
_, err := svc.UpdateRepositoryBaseBranch(ctx, UpdateRepositoryBaseBranchRequest{
TaskID: task.ID, TaskRepositoryID: rows[0].ID, BaseBranch: bad,
})
if err == nil {
t.Errorf("UpdateRepositoryBaseBranch(%q): expected error, got nil", bad)
}
}
if len(pusher.snapshot()) != 0 {
t.Errorf("unsafe inputs should not trigger pusher; got %d calls", len(pusher.snapshot()))
}
}
// TestUpdateRepositoryBaseBranch_NotFound covers the two missing-row cases:
// unknown task_repository_id, and a row that exists but belongs to a
// different task than the caller claimed. Both fold into the typed
// ErrTaskRepositoryNotFound so handlers can return 404 cleanly.
func TestUpdateRepositoryBaseBranch_NotFound(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
setBranchUpdateWorkflowStep(svc)
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "WS"})
_ = repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-1", WorkspaceID: "ws-1", Name: "WF"})
_ = repo.CreateRepository(ctx, &models.Repository{ID: "repo-1", WorkspaceID: "ws-1", Name: "frontend", DefaultBranch: "main"})
taskResult, _ := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
WorkflowID: "wf-1",
WorkflowStepID: "step-1",
Title: "Other task",
Repositories: []TaskRepositoryInput{
{RepositoryID: "repo-1", BaseBranch: "main"},
},
})
task := taskResult.Task
rows, _ := repo.ListTaskRepositories(ctx, task.ID)
t.Run("unknown row id", func(t *testing.T) {
_, err := svc.UpdateRepositoryBaseBranch(ctx, UpdateRepositoryBaseBranchRequest{
TaskID: task.ID,
TaskRepositoryID: "does-not-exist",
BaseBranch: "staging",
})
if !errors.Is(err, ErrTaskRepositoryNotFound) {
t.Errorf("got %v, want ErrTaskRepositoryNotFound", err)
}
})
t.Run("wrong task id", func(t *testing.T) {
_, err := svc.UpdateRepositoryBaseBranch(ctx, UpdateRepositoryBaseBranchRequest{
TaskID: "some-other-task",
TaskRepositoryID: rows[0].ID,
BaseBranch: "staging",
})
if !errors.Is(err, ErrTaskRepositoryNotFound) {
t.Errorf("got %v, want ErrTaskRepositoryNotFound", err)
}
})
}
// Regression: collectTaskBaseBranches skipped any task_repositories row whose
// Repository could not be resolved. For a multi-repo task that yields a
// non-empty but INCOMPLETE map — and since agentctl's SetBaseBranches replaces
// the stored map wholesale, pushing it silently drops the base branch of every
// repository that was skipped. The len>0 guard does not catch this, because the
// map is not empty; it is just missing entries. A partial hydration must be
// treated as a failure, not pushed.
func TestUpdateRepositoryBaseBranch_PartialHydrationIsNotPushed(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
setBranchUpdateWorkflowStep(svc)
pusher := &fakeBaseBranchPusher{}
svc.SetAgentBaseBranchPusher(pusher)
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "WS"})
_ = repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-1", WorkspaceID: "ws-1", Name: "WF"})
_ = repo.CreateRepository(ctx, &models.Repository{ID: "repo-1", WorkspaceID: "ws-1", Name: "frontend", DefaultBranch: "main"})
_ = repo.CreateRepository(ctx, &models.Repository{ID: "repo-2", WorkspaceID: "ws-1", Name: "backend", DefaultBranch: "main"})
taskResult, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
WorkflowID: "wf-1",
WorkflowStepID: "step-1",
Title: "Multi repo",
Repositories: []TaskRepositoryInput{
{RepositoryID: "repo-1", BaseBranch: "main", CheckoutBranch: "feature/a"},
{RepositoryID: "repo-2", BaseBranch: "main", CheckoutBranch: "feature/b"},
},
})
task := taskResult.Task
if err != nil {
t.Fatalf("CreateTask: %v", err)
}
rows, err := repo.ListTaskRepositories(ctx, task.ID)
if err != nil || len(rows) != 2 {
t.Fatalf("ListTaskRepositories: %v, rows=%d", err, len(rows))
}
// Delete one repository so its task_repositories row can no longer resolve
// a Name — the shape a failed GetRepository produces.
if err := repo.DeleteRepository(ctx, "repo-2"); err != nil {
t.Fatalf("DeleteRepository: %v", err)
}
var target string
for _, r := range rows {
if r.RepositoryID == "repo-1" {
target = r.ID
}
}
if _, err := svc.UpdateRepositoryBaseBranch(ctx, UpdateRepositoryBaseBranchRequest{
TaskID: task.ID,
TaskRepositoryID: target,
BaseBranch: "staging",
}); err != nil {
t.Fatalf("UpdateRepositoryBaseBranch: %v", err)
}
// The push must be skipped entirely. Pushing {frontend: staging} alone
// would replace the map and wipe backend's recorded base branch.
if calls := pusher.snapshot(); len(calls) != 0 {
t.Fatalf("expected no push for an incomplete hydration, got %d calls: %+v", len(calls), calls)
}
}
// Regression: collectTaskBaseBranches keyed the map by the bare Repository.Name.
// A task may attach the same repository on several branches, and those siblings
// live in `{RepoName}-{BranchSlug}` worktree directories — which is the name
// their WorkspaceTracker reports. Keying by name alone collapsed every sibling
// onto one entry, so the non-flat trackers found no key and fell back to
// origin/main. Worse, because SetBaseBranches *replaces* the stored map, this
// push overwrote the correctly-keyed map the launch path had already seeded.
//
// The keys must match lifecycle.baseBranchMetadataKey: the lowest-positioned
// branch of a repeated repository keeps the flat legacy path, the rest are
// suffixed with their branch-identity path slug.
func TestUpdateRepositoryBaseBranch_MultiBranchKeysPerWorktree(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
setBranchUpdateWorkflowStep(svc)
pusher := &fakeBaseBranchPusher{}
svc.SetAgentBaseBranchPusher(pusher)
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "WS"})
_ = repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-1", WorkspaceID: "ws-1", Name: "WF"})
_ = repo.CreateRepository(ctx, &models.Repository{ID: "repo-1", WorkspaceID: "ws-1", Name: "frontend", DefaultBranch: "main"})
taskResult, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
WorkflowID: "wf-1",
WorkflowStepID: "step-1",
Title: "Same repo, two branches",
Repositories: []TaskRepositoryInput{
{RepositoryID: "repo-1", BaseBranch: "main", CheckoutBranch: "feature/a"},
{RepositoryID: "repo-1", BaseBranch: "main", CheckoutBranch: "feature/b"},
},
})
task := taskResult.Task
if err != nil {
t.Fatalf("CreateTask: %v", err)
}
rows, err := repo.ListTaskRepositories(ctx, task.ID)
if err != nil || len(rows) != 2 {
t.Fatalf("ListTaskRepositories: %v, rows=%d", err, len(rows))
}
var sibling string
for _, r := range rows {
if r.CheckoutBranch == "feature/b" {
sibling = r.ID
}
}
if sibling == "" {
t.Fatal("no task_repositories row for feature/b")
}
if _, err := svc.UpdateRepositoryBaseBranch(ctx, UpdateRepositoryBaseBranchRequest{
TaskID: task.ID,
TaskRepositoryID: sibling,
BaseBranch: "staging",
}); err != nil {
t.Fatalf("UpdateRepositoryBaseBranch: %v", err)
}
calls := pusher.snapshot()
if len(calls) != 1 {
t.Fatalf("expected 1 pusher call, got %d", len(calls))
}
got := calls[0].branches
// feature/a is position 0, so it keeps the flat directory.
if got["frontend"] != "main" {
t.Errorf("branches[frontend] = %q, want main (flat sibling keeps its base)", got["frontend"])
}
// feature/b is the suffixed sibling and is the row we just updated.
if got["frontend-feature-b"] != "staging" {
t.Errorf("branches[frontend-feature-b] = %q, want staging", got["frontend-feature-b"])
}
// Multi-row tasks must not publish the empty-key fallback: lookupBaseBranch
// falls back to it for any unmatched tracker, which would hand one sibling's
// base branch to the other.
if _, ok := got[""]; ok {
t.Errorf("branches[\"\"] present for a multi-row task: %+v", got)
}
}
// The map keys are read by WorkspaceTracker, whose repositoryName is the
// worktree *directory* basename — a sanitised repo name, not the raw
// Repository.Name. A name with characters the directory sanitiser rewrites
// (e.g. a space) produced a key no tracker could ever match.
func TestCollectTaskBaseBranches_SanitizesRepositoryName(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
setBranchUpdateWorkflowStep(svc)
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "WS"})
_ = repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-1", WorkspaceID: "ws-1", Name: "WF"})
_ = repo.CreateRepository(ctx, &models.Repository{ID: "repo-1", WorkspaceID: "ws-1", Name: "my repo", DefaultBranch: "main"})
_ = repo.CreateRepository(ctx, &models.Repository{ID: "repo-2", WorkspaceID: "ws-1", Name: "backend", DefaultBranch: "main"})
taskResult, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1", WorkflowID: "wf-1", WorkflowStepID: "step-1", Title: "Unsanitised name",
Repositories: []TaskRepositoryInput{
{RepositoryID: "repo-1", BaseBranch: "main", CheckoutBranch: "feature/a"},
{RepositoryID: "repo-2", BaseBranch: "develop", CheckoutBranch: "feature/b"},
},
})
task := taskResult.Task
if err != nil {
t.Fatalf("CreateTask: %v", err)
}
branches, err := svc.collectTaskBaseBranches(ctx, task.ID)
if err != nil {
t.Fatalf("collectTaskBaseBranches: %v", err)
}
if branches["my-repo"] != "main" {
t.Errorf("branches[my-repo] = %q, want main; got map %+v", branches["my-repo"], branches)
}
if _, ok := branches["my repo"]; ok {
t.Errorf("raw repository name leaked as a key: %+v", branches)
}
if branches["backend"] != "develop" {
t.Errorf("branches[backend] = %q, want develop", branches["backend"])
}
}