You've already forked agentic-coding-workflow
201 lines
6.8 KiB
Go
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)
|
|
}
|