You've already forked agentic-coding-workflow
219 lines
7.2 KiB
Go
219 lines
7.2 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/infra/errs"
|
|
"github.com/yan1h/agent-coding-workflow/internal/workspace"
|
|
)
|
|
|
|
// ===== fakes =====
|
|
|
|
// fakeACPBridge implements ACPBridge.
|
|
type fakeACPBridge struct {
|
|
kinds []ACPAgentKind
|
|
sessions []ACPSession
|
|
perms []ACPPermission
|
|
terminated []uuid.UUID
|
|
}
|
|
|
|
func (f *fakeACPBridge) ListAgentKinds(_ context.Context, _ uuid.UUID, _ bool) ([]ACPAgentKind, error) {
|
|
return f.kinds, nil
|
|
}
|
|
func (f *fakeACPBridge) GetAgentKind(_ context.Context, _ uuid.UUID, _ bool, id uuid.UUID) (*ACPAgentKind, error) {
|
|
for i := range f.kinds {
|
|
if f.kinds[i].ID == id {
|
|
return &f.kinds[i], nil
|
|
}
|
|
}
|
|
return nil, errs.New(errs.CodeNotFound, "agent kind")
|
|
}
|
|
func (f *fakeACPBridge) CreateSession(_ context.Context, userID uuid.UUID, _ bool, in ACPCreateSessionInput) (*ACPSession, error) {
|
|
s := ACPSession{
|
|
ID: uuid.New(), WorkspaceID: in.WorkspaceID, ProjectID: testPID,
|
|
AgentKindID: in.AgentKindID, UserID: userID,
|
|
Status: "starting", StartedAt: time.Now(),
|
|
}
|
|
if in.Branch != nil {
|
|
s.Branch = *in.Branch
|
|
}
|
|
f.sessions = append(f.sessions, s)
|
|
return &s, nil
|
|
}
|
|
func (f *fakeACPBridge) GetSession(_ context.Context, _ uuid.UUID, _ bool, id uuid.UUID) (*ACPSession, error) {
|
|
for i := range f.sessions {
|
|
if f.sessions[i].ID == id {
|
|
return &f.sessions[i], nil
|
|
}
|
|
}
|
|
return nil, errs.New(errs.CodeNotFound, "session")
|
|
}
|
|
func (f *fakeACPBridge) ListSessions(_ context.Context, _ uuid.UUID, _ bool, _ ACPSessionFilter) ([]ACPSession, error) {
|
|
return f.sessions, nil
|
|
}
|
|
func (f *fakeACPBridge) TerminateSession(_ context.Context, _ uuid.UUID, _ bool, id uuid.UUID) error {
|
|
f.terminated = append(f.terminated, id)
|
|
return nil
|
|
}
|
|
func (f *fakeACPBridge) ListPendingPermissions(_ context.Context, _ uuid.UUID, _ bool, sessionID uuid.UUID) ([]ACPPermission, error) {
|
|
out := make([]ACPPermission, 0, len(f.perms))
|
|
for _, p := range f.perms {
|
|
if p.SessionID == sessionID {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func acpTestDeps() (ServerDeps, *fakeACPBridge) {
|
|
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(),
|
|
}}}
|
|
bridge := &fakeACPBridge{}
|
|
deps.ACP = bridge
|
|
return deps, bridge
|
|
}
|
|
|
|
// ===== tests =====
|
|
|
|
func TestListAndGetAgentKinds(t *testing.T) {
|
|
deps, bridge := acpTestDeps()
|
|
bridge.kinds = []ACPAgentKind{{
|
|
ID: uuid.New(), Name: "claude_code", DisplayName: "Claude Code",
|
|
Enabled: true, ToolAllowlist: []string{"*"},
|
|
}}
|
|
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "list_agent_kinds", map[string]any{})
|
|
require.NoError(t, err)
|
|
require.False(t, result.IsError)
|
|
var listOut listAgentKindsOut
|
|
unmarshalStructured(t, result, &listOut)
|
|
require.Len(t, listOut.AgentKinds, 1)
|
|
assert.Equal(t, "claude_code", listOut.AgentKinds[0].Name)
|
|
|
|
result, err = callTool(ctxWithAuth(Scope{}), deps, "get_agent_kind", map[string]any{
|
|
"agent_kind_id": bridge.kinds[0].ID.String(),
|
|
})
|
|
require.NoError(t, err)
|
|
var getOut agentKindDetail
|
|
unmarshalStructured(t, result, &getOut)
|
|
assert.Equal(t, "Claude Code", getOut.DisplayName)
|
|
}
|
|
|
|
func TestACPSessionLifecycle(t *testing.T) {
|
|
deps, bridge := acpTestDeps()
|
|
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "create_acp_session", map[string]any{
|
|
"workspace_id": testWSID.String(), "agent_kind_id": uuid.New().String(),
|
|
"branch": "feat/x", "initial_prompt": "do something",
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, result.IsError)
|
|
var createOut acpSessionCreateOut
|
|
unmarshalStructured(t, result, &createOut)
|
|
assert.True(t, createOut.OK)
|
|
assert.Equal(t, "feat/x", createOut.Session.Branch)
|
|
sid := createOut.Session.ID
|
|
|
|
result, err = callTool(ctxWithAuth(Scope{}), deps, "get_acp_session", map[string]any{
|
|
"session_id": sid,
|
|
})
|
|
require.NoError(t, err)
|
|
var getOut acpSessionDetail
|
|
unmarshalStructured(t, result, &getOut)
|
|
assert.Equal(t, sid, getOut.ID)
|
|
|
|
result, err = callTool(ctxWithAuth(Scope{}), deps, "list_acp_sessions", map[string]any{})
|
|
require.NoError(t, err)
|
|
var listOut listACPSessionsOut
|
|
unmarshalStructured(t, result, &listOut)
|
|
require.Len(t, listOut.Sessions, 1)
|
|
|
|
result, err = callTool(ctxWithAuth(Scope{}), deps, "terminate_acp_session", map[string]any{
|
|
"session_id": sid,
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, result.IsError)
|
|
require.Len(t, bridge.terminated, 1)
|
|
assert.Equal(t, sid, bridge.terminated[0].String())
|
|
}
|
|
|
|
func TestCreateACPSession_DeniedForSystemToken(t *testing.T) {
|
|
deps, bridge := acpTestDeps()
|
|
|
|
// system token(agent 会话签发)→ 不允许套娃创建嵌套 session
|
|
ctx := context.WithValue(context.Background(), AuthContextKey, &AuthSession{
|
|
UserID: testUID.String(),
|
|
TokenID: uuid.NewString(),
|
|
Issuer: IssuerSystem,
|
|
})
|
|
result, err := callTool(ctx, deps, "create_acp_session", map[string]any{
|
|
"workspace_id": testWSID.String(), "agent_kind_id": uuid.New().String(),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, result.IsError)
|
|
assert.Empty(t, bridge.sessions)
|
|
|
|
// admin token(Issuer admin)→ 允许
|
|
ctx = context.WithValue(context.Background(), AuthContextKey, &AuthSession{
|
|
UserID: testUID.String(),
|
|
TokenID: uuid.NewString(),
|
|
Issuer: IssuerAdmin,
|
|
})
|
|
result, err = callTool(ctx, deps, "create_acp_session", map[string]any{
|
|
"workspace_id": testWSID.String(), "agent_kind_id": uuid.New().String(),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.False(t, result.IsError)
|
|
assert.Len(t, bridge.sessions, 1)
|
|
}
|
|
|
|
func TestListACPSessions_ScopeFiltered(t *testing.T) {
|
|
deps, bridge := acpTestDeps()
|
|
otherProject := uuid.New()
|
|
bridge.sessions = []ACPSession{
|
|
{ID: uuid.New(), WorkspaceID: testWSID, ProjectID: testPID, AgentKindID: uuid.New(), UserID: testUID, Status: "running", StartedAt: time.Now()},
|
|
{ID: uuid.New(), WorkspaceID: uuid.New(), ProjectID: otherProject, AgentKindID: uuid.New(), UserID: testUID, Status: "running", StartedAt: time.Now()},
|
|
}
|
|
|
|
result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
|
|
"list_acp_sessions", map[string]any{})
|
|
require.NoError(t, err)
|
|
var out listACPSessionsOut
|
|
unmarshalStructured(t, result, &out)
|
|
require.Len(t, out.Sessions, 1, "scope 外项目的 session 应被过滤")
|
|
assert.Equal(t, testPID.String(), out.Sessions[0].ProjectID)
|
|
}
|
|
|
|
func TestListPendingPermissions(t *testing.T) {
|
|
deps, bridge := acpTestDeps()
|
|
sid := uuid.New()
|
|
bridge.sessions = []ACPSession{{
|
|
ID: sid, WorkspaceID: testWSID, ProjectID: testPID,
|
|
AgentKindID: uuid.New(), UserID: testUID, Status: "running", StartedAt: time.Now(),
|
|
}}
|
|
bridge.perms = []ACPPermission{{
|
|
ID: uuid.New(), SessionID: sid, AgentRequestID: "req-1",
|
|
ToolName: "fs/write", Status: "pending", CreatedAt: time.Now(),
|
|
}}
|
|
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "list_pending_permissions", map[string]any{
|
|
"session_id": sid.String(),
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, result.IsError)
|
|
var out listPendingPermissionsOut
|
|
unmarshalStructured(t, result, &out)
|
|
require.Len(t, out.Permissions, 1)
|
|
assert.Equal(t, "fs/write", out.Permissions[0].ToolName)
|
|
assert.Equal(t, "pending", out.Permissions[0].Status)
|
|
}
|