Files
agentic-coding-workflow/internal/mcp/tools_changerequest_test.go
2026-06-22 08:55:57 +08:00

201 lines
6.8 KiB
Go

package mcp
import (
"context"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yan1h/agent-coding-workflow/internal/changerequest"
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
"github.com/yan1h/agent-coding-workflow/internal/workspace"
)
// fakeChangeReqSvc implements changerequest.Service.
type fakeChangeReqSvc struct {
items []*changerequest.ChangeRequest
mergeErr error
syncState changerequest.CIState
}
func (f *fakeChangeReqSvc) Create(_ context.Context, c changerequest.Caller, wsID uuid.UUID, in changerequest.CreateInput) (*changerequest.ChangeRequest, error) {
ext := int64(len(f.items) + 1)
cr := &changerequest.ChangeRequest{
ID: uuid.New(), ProjectID: testPID, WorkspaceID: wsID, Number: len(f.items) + 1,
Title: in.Title, SourceBranch: in.SourceBranch, TargetBranch: firstNonEmpty(in.TargetBranch, "main"),
State: changerequest.StateOpen, ReviewVerdict: changerequest.VerdictPending, CIState: changerequest.CIUnknown,
Provider: "gitea", ExternalID: &ext, ExternalURL: "http://h/pr", CreatedBy: c.UserID,
}
f.items = append(f.items, cr)
return cr, nil
}
func (f *fakeChangeReqSvc) Get(_ context.Context, _ changerequest.Caller, id uuid.UUID) (*changerequest.ChangeRequest, error) {
for _, cr := range f.items {
if cr.ID == id {
return cr, nil
}
}
return nil, errs.New(errs.CodeNotFound, "not found")
}
func (f *fakeChangeReqSvc) GetByNumber(_ context.Context, _ changerequest.Caller, _ string, n int) (*changerequest.ChangeRequest, error) {
for _, cr := range f.items {
if cr.Number == n {
return cr, nil
}
}
return nil, errs.New(errs.CodeNotFound, "not found")
}
func (f *fakeChangeReqSvc) ListByWorkspace(_ context.Context, _ changerequest.Caller, wsID uuid.UUID) ([]*changerequest.ChangeRequest, error) {
out := make([]*changerequest.ChangeRequest, 0, len(f.items))
for _, cr := range f.items {
if cr.WorkspaceID == wsID {
out = append(out, cr)
}
}
return out, nil
}
func (f *fakeChangeReqSvc) Review(_ context.Context, _ changerequest.Caller, id uuid.UUID, v changerequest.Verdict, _ string) (*changerequest.ChangeRequest, error) {
cr, err := f.Get(context.Background(), changerequest.Caller{}, id)
if err != nil {
return nil, err
}
cr.ReviewVerdict = v
return cr, nil
}
func (f *fakeChangeReqSvc) Merge(_ context.Context, _ changerequest.Caller, id uuid.UUID, _ string) (*changerequest.ChangeRequest, error) {
if f.mergeErr != nil {
return nil, f.mergeErr
}
cr, err := f.Get(context.Background(), changerequest.Caller{}, id)
if err != nil {
return nil, err
}
cr.State = changerequest.StateMerged
cr.MergeCommitSHA = "merged-sha"
return cr, nil
}
func (f *fakeChangeReqSvc) SyncStatus(_ context.Context, _ changerequest.Caller, id uuid.UUID) (*changerequest.ChangeRequest, error) {
cr, err := f.Get(context.Background(), changerequest.Caller{}, id)
if err != nil {
return nil, err
}
if f.syncState != "" {
cr.CIState = f.syncState
}
return cr, nil
}
func firstNonEmpty(a, b string) string {
if a != "" {
return a
}
return b
}
// crTestDeps mirrors wsTestDeps but with a change-request service attached.
func crTestDeps() (ServerDeps, *fakeChangeReqSvc) {
deps := testDeps()
deps.Workspaces = &fakeWorkspaceSvc{items: []*workspace.Workspace{{
ID: testWSID, ProjectID: testPID, Slug: "ws-1", Name: "Workspace 1",
DefaultBranch: "main", SyncStatus: workspace.SyncStatusIdle, CreatedAt: time.Now(),
}}}
deps.Worktrees = &fakeWorktreeSvc{}
deps.GitOps = &fakeGitOpsSvc{}
crSvc := &fakeChangeReqSvc{}
deps.ChangeReqs = crSvc
return deps, crSvc
}
func TestCreatePullRequest_HappyPath(t *testing.T) {
deps, crSvc := crTestDeps()
result, err := callTool(ctxWithAuth(Scope{}), deps, "create_pull_request", map[string]any{
"workspace_id": testWSID.String(), "source_branch": "feature", "title": "Add X",
})
require.NoError(t, err)
require.False(t, result.IsError)
var out changeRequestOut
unmarshalStructured(t, result, &out)
assert.True(t, out.OK)
assert.Equal(t, "feature", out.ChangeRequest.SourceBranch)
require.Len(t, crSvc.items, 1)
}
func TestCreatePullRequest_ScopeDenied(t *testing.T) {
deps, _ := crTestDeps()
result, err := callTool(ctxWithAuth(Scope{Tools: []string{"git_diff"}}), deps, "create_pull_request", map[string]any{
"workspace_id": testWSID.String(), "source_branch": "feature", "title": "X",
})
require.NoError(t, err)
assert.True(t, result.IsError, "token scope without create_pull_request must deny")
}
func TestGetPRStatus(t *testing.T) {
deps, crSvc := crTestDeps()
crSvc.syncState = changerequest.CISuccess
// seed via create
_, err := callTool(ctxWithAuth(Scope{}), deps, "create_pull_request", map[string]any{
"workspace_id": testWSID.String(), "source_branch": "feature", "title": "X",
})
require.NoError(t, err)
result, err := callTool(ctxWithAuth(Scope{}), deps, "get_pr_status", map[string]any{
"workspace_id": testWSID.String(), "number": 1,
})
require.NoError(t, err)
require.False(t, result.IsError)
var out changeRequestOut
unmarshalStructured(t, result, &out)
assert.Equal(t, string(changerequest.CISuccess), out.ChangeRequest.CIState)
}
func TestMergePullRequest_GateError(t *testing.T) {
deps, crSvc := crTestDeps()
crSvc.mergeErr = errs.New(errs.CodeFailedPrecondition, "merge blocked: review not approved")
_, err := callTool(ctxWithAuth(Scope{}), deps, "create_pull_request", map[string]any{
"workspace_id": testWSID.String(), "source_branch": "feature", "title": "X",
})
require.NoError(t, err)
result, err := callTool(ctxWithAuth(Scope{}), deps, "merge_pull_request", map[string]any{
"workspace_id": testWSID.String(), "number": 1,
})
require.NoError(t, err)
assert.True(t, result.IsError, "gated merge must surface as an MCP error")
}
func TestMergePullRequest_Allowed(t *testing.T) {
deps, _ := crTestDeps()
_, err := callTool(ctxWithAuth(Scope{}), deps, "create_pull_request", map[string]any{
"workspace_id": testWSID.String(), "source_branch": "feature", "title": "X",
})
require.NoError(t, err)
result, err := callTool(ctxWithAuth(Scope{}), deps, "merge_pull_request", map[string]any{
"workspace_id": testWSID.String(), "number": 1, "method": "squash",
})
require.NoError(t, err)
require.False(t, result.IsError)
var out changeRequestOut
unmarshalStructured(t, result, &out)
assert.Equal(t, string(changerequest.StateMerged), out.ChangeRequest.State)
}
func TestListPullRequests(t *testing.T) {
deps, _ := crTestDeps()
_, err := callTool(ctxWithAuth(Scope{}), deps, "create_pull_request", map[string]any{
"workspace_id": testWSID.String(), "source_branch": "feature", "title": "X",
})
require.NoError(t, err)
result, err := callTool(ctxWithAuth(Scope{}), deps, "list_pull_requests", map[string]any{
"workspace_id": testWSID.String(),
})
require.NoError(t, err)
var out listPRsOut
unmarshalStructured(t, result, &out)
require.Len(t, out.ChangeRequests, 1)
}