From 665d029d2bd3019089ea408a801c701e3f93cd11 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Wed, 10 Jun 2026 18:05:18 +0800 Subject: [PATCH] feat(workspace): add ListBranches method to WorkspaceService Co-Authored-By: Claude Opus 4.6 --- internal/acp/handler_e2e_test.go | 3 ++ internal/acp/session_service_test.go | 3 ++ internal/app/acp_integration_test.go | 4 +++ internal/mcp/tools_pm_test.go | 3 ++ internal/workspace/domain.go | 1 + internal/workspace/handler_test.go | 4 +++ internal/workspace/workspace_service.go | 17 ++++++++++ internal/workspace/workspace_service_test.go | 33 ++++++++++++++++++++ 8 files changed, 68 insertions(+) diff --git a/internal/acp/handler_e2e_test.go b/internal/acp/handler_e2e_test.go index 0c6db36..36b479e 100644 --- a/internal/acp/handler_e2e_test.go +++ b/internal/acp/handler_e2e_test.go @@ -68,6 +68,9 @@ func (s *stubWsService) UpsertCredential(context.Context, workspace.Caller, uuid func (s *stubWsService) GetCredentialMeta(context.Context, workspace.Caller, uuid.UUID) (*workspace.Credential, error) { panic("stubWsService.GetCredentialMeta unused") } +func (s *stubWsService) ListBranches(context.Context, workspace.Caller, uuid.UUID) ([]string, error) { + panic("stubWsService.ListBranches unused") +} // stubProjectAccess satisfies acp.ProjectAccess for the e2e test. // Only GetProjectByWorkspace is exercised; the rest panic. diff --git a/internal/acp/session_service_test.go b/internal/acp/session_service_test.go index ce5628c..ea2d2ef 100644 --- a/internal/acp/session_service_test.go +++ b/internal/acp/session_service_test.go @@ -327,6 +327,9 @@ func (s *fakeWsSvc) UpsertCredential(context.Context, workspace.Caller, uuid.UUI func (s *fakeWsSvc) GetCredentialMeta(context.Context, workspace.Caller, uuid.UUID) (*workspace.Credential, error) { return nil, fmt.Errorf("unused") } +func (s *fakeWsSvc) ListBranches(context.Context, workspace.Caller, uuid.UUID) ([]string, error) { + return nil, fmt.Errorf("unused") +} // ===== Tests ===== diff --git a/internal/app/acp_integration_test.go b/internal/app/acp_integration_test.go index 0655e9a..d395707 100644 --- a/internal/app/acp_integration_test.go +++ b/internal/app/acp_integration_test.go @@ -436,6 +436,10 @@ func (s *acpStubWsSvc) GetCredentialMeta(context.Context, workspace.Caller, uuid return nil, fmt.Errorf("acpStubWsSvc.GetCredentialMeta unused") } +func (s *acpStubWsSvc) ListBranches(context.Context, workspace.Caller, uuid.UUID) ([]string, error) { + return nil, fmt.Errorf("acpStubWsSvc.ListBranches unused") +} + // acpStubProjectAccess satisfies acp.ProjectAccess; only GetProjectByWorkspace is // exercised by the branch=main path. type acpStubProjectAccess struct { diff --git a/internal/mcp/tools_pm_test.go b/internal/mcp/tools_pm_test.go index 1c0fa47..a1372dd 100644 --- a/internal/mcp/tools_pm_test.go +++ b/internal/mcp/tools_pm_test.go @@ -266,6 +266,9 @@ func (f *fakeWorkspaceSvc) UpsertCredential(_ context.Context, _ workspace.Calle func (f *fakeWorkspaceSvc) GetCredentialMeta(_ context.Context, _ workspace.Caller, _ uuid.UUID) (*workspace.Credential, error) { panic("not implemented") } +func (f *fakeWorkspaceSvc) ListBranches(_ context.Context, _ workspace.Caller, _ uuid.UUID) ([]string, error) { + panic("not implemented") +} // ===== test helpers ===== diff --git a/internal/workspace/domain.go b/internal/workspace/domain.go index d4a870a..759727f 100644 --- a/internal/workspace/domain.go +++ b/internal/workspace/domain.go @@ -145,6 +145,7 @@ type WorkspaceService interface { UpsertCredential(ctx context.Context, c Caller, wsID uuid.UUID, in UpsertCredentialInput) (*Credential, error) GetCredentialMeta(ctx context.Context, c Caller, wsID uuid.UUID) (*Credential, error) + ListBranches(ctx context.Context, c Caller, wsID uuid.UUID) ([]string, error) } // WorktreeService 暴露支线 worktree 的应用服务方法。 diff --git a/internal/workspace/handler_test.go b/internal/workspace/handler_test.go index cc53476..a6ab07b 100644 --- a/internal/workspace/handler_test.go +++ b/internal/workspace/handler_test.go @@ -48,6 +48,10 @@ func (f *fakeWS) GetCredentialMeta(_ context.Context, _ Caller, _ uuid.UUID) (*C return &Credential{Kind: CredKindNone}, nil } +func (f *fakeWS) ListBranches(_ context.Context, _ Caller, _ uuid.UUID) ([]string, error) { + return nil, nil +} + // fakeResolver 把固定 token 解码为 uuid(沿 internal/project handler_test.go 模式)。 type fakeResolver struct{ valid map[string]uuid.UUID } diff --git a/internal/workspace/workspace_service.go b/internal/workspace/workspace_service.go index 7d10bfe..43bb120 100644 --- a/internal/workspace/workspace_service.go +++ b/internal/workspace/workspace_service.go @@ -328,6 +328,23 @@ func (s *workspaceService) GetCredentialMeta(ctx context.Context, c Caller, wsID return cred, nil } +func (s *workspaceService) ListBranches(ctx context.Context, c Caller, wsID uuid.UUID) ([]string, error) { + ws, err := s.repo.GetWorkspaceByID(ctx, wsID) + if err != nil { + return nil, err + } + if canRead, _, err := s.pa.ResolveByID(ctx, c.UserID, c.IsAdmin, ws.ProjectID); err != nil { + return nil, err + } else if !canRead { + return nil, errs.New(errs.CodeForbidden, "no read access") + } + cred, err := s.loadDecryptedCredential(ctx, wsID) + if err != nil { + return nil, err + } + return s.gitr.ListRemoteBranches(ctx, ws.MainPath, cred) +} + func (s *workspaceService) encryptForUpsert(wsID uuid.UUID, in UpsertCredentialInput) (*Credential, error) { cred := &Credential{ ID: uuid.New(), diff --git a/internal/workspace/workspace_service_test.go b/internal/workspace/workspace_service_test.go index 372de38..1d2cd3b 100644 --- a/internal/workspace/workspace_service_test.go +++ b/internal/workspace/workspace_service_test.go @@ -450,3 +450,36 @@ func TestWorkspaceService_Update_DefaultBranch_SameValue_NoGitOps(t *testing.T) require.Empty(t, fg.fetchRemoteBranchCalls) require.Empty(t, fg.checkoutCalls) } + +func TestWorkspaceService_ListBranches(t *testing.T) { + t.Parallel() + pa := &fakePA{pid: uuid.New(), canRead: true, canWrite: true} + svc, repo := newSvc(t, pa, nil, &fakeGit{}) + caller := Caller{UserID: uuid.New()} + + wsID := uuid.New() + repo.wss[wsID] = &Workspace{ + ID: wsID, ProjectID: pa.pid, Slug: "ws1", + DefaultBranch: "main", MainPath: "/data/ws1/main", + } + + branches, err := svc.ListBranches(context.Background(), caller, wsID) + require.NoError(t, err) + // fakeGit returns nil, nil → empty slice + require.Nil(t, branches) +} + +func TestWorkspaceService_ListBranches_NoReadAccess(t *testing.T) { + t.Parallel() + pa := &fakePA{pid: uuid.New(), canRead: false, canWrite: false} + svc, repo := newSvc(t, pa, nil, &fakeGit{}) + caller := Caller{UserID: uuid.New()} + + wsID := uuid.New() + repo.wss[wsID] = &Workspace{ + ID: wsID, ProjectID: pa.pid, Slug: "ws1", + } + + _, err := svc.ListBranches(context.Background(), caller, wsID) + require.Error(t, err) +}