ACP / MCP 完善

This commit is contained in:
2026-06-11 12:43:18 +08:00
parent 1415d3b43b
commit 0561e126cd
22 changed files with 786 additions and 219 deletions
+108
View File
@@ -245,6 +245,19 @@ func (r *fakeAcpRepo) UpdateSessionRunning(_ context.Context, _ uuid.UUID, _ str
func (r *fakeAcpRepo) UpdateSessionFinished(_ context.Context, _ uuid.UUID, _ acp.SessionStatus, _ *int32, _ *string) error {
return nil
}
func (r *fakeAcpRepo) MarkSessionFailedIfActive(_ context.Context, id uuid.UUID, lastErr string) (bool, error) {
r.mu.Lock()
defer r.mu.Unlock()
for _, s := range r.sessions {
if s.ID == id && s.Status.IsActive() {
s.Status = acp.SessionCrashed
msg := lastErr
s.LastError = &msg
return true, nil
}
}
return false, nil
}
func (r *fakeAcpRepo) ResetStuckSessionsOnRestart(_ context.Context) ([]*acp.Session, error) {
return nil, nil
}
@@ -444,6 +457,101 @@ func TestSessionService_Create_TokenIssueFails_RollsBack(t *testing.T) {
repo.mu.Unlock()
}
func TestSessionService_Create_SpawnFails_MarksSessionCrashed(t *testing.T) {
t.Parallel()
uid := uuid.New()
wsID := uuid.New()
pid := uuid.New()
akID := uuid.New()
repo := &fakeAcpRepo{
kinds: []*acp.AgentKind{
// BinaryPath 不存在 → supervisor.Spawn 在 cmd.Start 早期失败,
// 进程未注册、onExit 不会触发。
{ID: akID, Name: "claude", Enabled: true, BinaryPath: "/nonexistent/agent-binary"},
},
}
wsSvc := &fakeWsSvc{ws: &workspace.Workspace{
ID: wsID, ProjectID: pid, DefaultBranch: "main", MainPath: "/tmp/main",
}}
pa := fakeProjectAccess{pj: &project.Project{ID: pid, Slug: "test-proj"}}
mcpTokens := &fakeMCPTokenSvc{}
sup := acp.NewSupervisor(
repo, nil, nil, nil, nil, mcpTokens, nil,
acp.SupervisorConfig{SpawnTimeout: 5 * time.Second, KillGrace: 1 * time.Second},
nil,
)
svc := acp.NewSessionService(
repo, wsSvc, nil, nil, pa, sup, nil,
acp.SessionServiceConfig{
MaxActiveGlobal: 10,
MaxActivePerUser: 5,
SystemTokenTTL: 24 * time.Hour,
MCPPublicURL: "http://localhost:8080/mcp",
},
mcpTokens,
)
branch := "main"
sess, err := svc.Create(context.Background(), acp.Caller{UserID: uid}, acp.CreateSessionInput{
WorkspaceID: wsID,
AgentKindID: akID,
Branch: &branch,
})
require.NoError(t, err)
require.NotNil(t, sess)
// async spawn 失败后:session 被守卫式标记为 crashed 且 last_error 非空
require.Eventually(t, func() bool {
repo.mu.Lock()
defer repo.mu.Unlock()
for _, s := range repo.sessions {
if s.ID == sess.ID {
return s.Status == acp.SessionCrashed && s.LastError != nil && *s.LastError != ""
}
}
return false
}, 5*time.Second, 50*time.Millisecond, "session should be marked crashed with non-empty last_error")
}
func TestSessionService_Create_MutuallyExclusiveInputs(t *testing.T) {
t.Parallel()
uid := uuid.New()
wsID := uuid.New()
akID := uuid.New()
svc := acp.NewSessionService(
&fakeAcpRepo{}, nil, nil, nil, nil, nil, nil,
acp.SessionServiceConfig{}, nil,
)
branch := "feat/x"
issueNum := 42
reqNum := 7
cases := []struct {
name string
in acp.CreateSessionInput
}{
{"branch+issue", acp.CreateSessionInput{Branch: &branch, IssueNumber: &issueNum}},
{"branch+requirement", acp.CreateSessionInput{Branch: &branch, RequirementNumber: &reqNum}},
{"issue+requirement", acp.CreateSessionInput{IssueNumber: &issueNum, RequirementNumber: &reqNum}},
{"all three", acp.CreateSessionInput{Branch: &branch, IssueNumber: &issueNum, RequirementNumber: &reqNum}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
tc.in.WorkspaceID = wsID
tc.in.AgentKindID = akID
_, err := svc.Create(context.Background(), acp.Caller{UserID: uid}, tc.in)
require.Error(t, err)
assert.Contains(t, err.Error(), "mutually exclusive")
})
}
}
// ===== PermissionRequest(权限审批框架,测试桩) =====
func (f *fakeAcpRepo) InsertPermissionRequest(context.Context, *acp.PermissionRequest) (*acp.PermissionRequest, error) {
return &acp.PermissionRequest{}, nil