feat(acp): AgentKindService + unit tests (fake repo + real encryptor)

This commit is contained in:
2026-05-07 11:28:15 +08:00
parent 1ef2e93b72
commit 79480b32f8
2 changed files with 402 additions and 0 deletions
+220
View File
@@ -0,0 +1,220 @@
package acp_test
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/acp"
"github.com/yan1h/agent-coding-workflow/internal/audit"
"github.com/yan1h/agent-coding-workflow/internal/infra/crypto"
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)
// fakeAgentKindRepo 是仅覆盖 AgentKind 方法的 fake;其他方法 panic
// (隔离单测,不引 testcontainers)。
type fakeAgentKindRepo struct {
store map[uuid.UUID]*acp.AgentKind
}
func newFakeAgentKindRepo() *fakeAgentKindRepo {
return &fakeAgentKindRepo{store: map[uuid.UUID]*acp.AgentKind{}}
}
func (f *fakeAgentKindRepo) CreateAgentKind(_ context.Context, k *acp.AgentKind) (*acp.AgentKind, error) {
cp := *k
f.store[k.ID] = &cp
return &cp, nil
}
func (f *fakeAgentKindRepo) GetAgentKindByID(_ context.Context, id uuid.UUID) (*acp.AgentKind, error) {
if k, ok := f.store[id]; ok {
cp := *k
return &cp, nil
}
return nil, errs.New(errs.CodeAcpAgentKindNotFound, "not found")
}
func (f *fakeAgentKindRepo) GetAgentKindByName(_ context.Context, name string) (*acp.AgentKind, error) {
for _, k := range f.store {
if k.Name == name {
cp := *k
return &cp, nil
}
}
return nil, errs.New(errs.CodeAcpAgentKindNotFound, "not found")
}
func (f *fakeAgentKindRepo) ListAgentKinds(_ context.Context) ([]*acp.AgentKind, error) {
out := make([]*acp.AgentKind, 0, len(f.store))
for _, k := range f.store {
cp := *k
out = append(out, &cp)
}
return out, nil
}
func (f *fakeAgentKindRepo) ListEnabledAgentKinds(_ context.Context) ([]*acp.AgentKind, error) {
out := make([]*acp.AgentKind, 0)
for _, k := range f.store {
if k.Enabled {
cp := *k
out = append(out, &cp)
}
}
return out, nil
}
func (f *fakeAgentKindRepo) UpdateAgentKind(_ context.Context, k *acp.AgentKind) (*acp.AgentKind, error) {
if _, ok := f.store[k.ID]; !ok {
return nil, errs.New(errs.CodeAcpAgentKindNotFound, "not found")
}
cp := *k
f.store[k.ID] = &cp
return &cp, nil
}
func (f *fakeAgentKindRepo) DeleteAgentKind(_ context.Context, id uuid.UUID) error {
delete(f.store, id)
return nil
}
func (f *fakeAgentKindRepo) CountAgentKindUsage(_ context.Context, _ uuid.UUID) (int64, error) {
return 0, nil
}
// 其他方法 panic(不该被 AgentKindService 调用)
func (f *fakeAgentKindRepo) InsertSession(context.Context, *acp.Session) (*acp.Session, error) {
panic("n/a")
}
func (f *fakeAgentKindRepo) GetSessionByID(context.Context, uuid.UUID) (*acp.Session, error) {
panic("n/a")
}
func (f *fakeAgentKindRepo) ListSessionsByUser(context.Context, uuid.UUID) ([]*acp.Session, error) {
panic("n/a")
}
func (f *fakeAgentKindRepo) ListAllSessions(context.Context) ([]*acp.Session, error) {
panic("n/a")
}
func (f *fakeAgentKindRepo) UpdateSessionRunning(context.Context, uuid.UUID, string, int32) error {
panic("n/a")
}
func (f *fakeAgentKindRepo) UpdateSessionFinished(context.Context, uuid.UUID, acp.SessionStatus, *int32, *string) error {
panic("n/a")
}
func (f *fakeAgentKindRepo) CountActiveSessions(context.Context) (int64, error) {
panic("n/a")
}
func (f *fakeAgentKindRepo) CountActiveSessionsByUser(context.Context, uuid.UUID) (int64, error) {
panic("n/a")
}
func (f *fakeAgentKindRepo) ResetStuckSessionsOnRestart(context.Context) ([]*acp.Session, error) {
panic("n/a")
}
func (f *fakeAgentKindRepo) AcquireMainWorktree(context.Context, uuid.UUID, uuid.UUID) (bool, error) {
panic("n/a")
}
func (f *fakeAgentKindRepo) ReleaseMainWorktree(context.Context, uuid.UUID, uuid.UUID) (bool, error) {
panic("n/a")
}
func (f *fakeAgentKindRepo) InsertEvent(context.Context, *acp.Event) (*acp.Event, error) {
panic("n/a")
}
func (f *fakeAgentKindRepo) ListEventsSince(context.Context, uuid.UUID, int64, int32) ([]*acp.Event, error) {
panic("n/a")
}
func (f *fakeAgentKindRepo) PurgeEventsBefore(context.Context, time.Time) (int64, error) {
panic("n/a")
}
// agentKindRecordingRecorder:accumulate audit entries
type agentKindRecordingRecorder struct{ entries []audit.Entry }
func (r *agentKindRecordingRecorder) Record(_ context.Context, e audit.Entry) error {
r.entries = append(r.entries, e)
return nil
}
// testEncryptor returns a real *crypto.Encryptor with a fixed test key.
func testEncryptor(t *testing.T) *crypto.Encryptor {
t.Helper()
key := []byte("0123456789abcdef0123456789abcdef") // 32 bytes
enc, err := crypto.NewEncryptor(key)
require.NoError(t, err)
return enc
}
func TestAgentKindService_Create(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo := newFakeAgentKindRepo()
rec := &agentKindRecordingRecorder{}
svc := acp.NewAgentKindService(repo, testEncryptor(t), rec)
admin := acp.Caller{UserID: uuid.New(), IsAdmin: true}
k, err := svc.Create(ctx, admin, acp.CreateAgentKindInput{
Name: "claude_code", DisplayName: "Claude Code",
BinaryPath: "claude-code", Args: []string{"--acp"},
Env: map[string]string{"K": "v"}, Enabled: true,
})
require.NoError(t, err)
assert.Equal(t, "claude_code", k.Name)
assert.NotEmpty(t, k.EncryptedEnv) // env 已加密
require.Len(t, rec.entries, 1)
assert.Equal(t, "acp.agent_kind.create", rec.entries[0].Action)
}
func TestAgentKindService_Create_NonAdmin(t *testing.T) {
t.Parallel()
ctx := context.Background()
svc := acp.NewAgentKindService(newFakeAgentKindRepo(), testEncryptor(t), &agentKindRecordingRecorder{})
user := acp.Caller{UserID: uuid.New(), IsAdmin: false}
_, err := svc.Create(ctx, user, acp.CreateAgentKindInput{
Name: "x", DisplayName: "x", BinaryPath: "x", Enabled: true,
})
ae, ok := errs.As(err)
require.True(t, ok)
assert.Equal(t, errs.CodeForbidden, ae.Code)
}
func TestAgentKindService_List_NonAdmin_OnlyEnabled(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo := newFakeAgentKindRepo()
svc := acp.NewAgentKindService(repo, testEncryptor(t), nil)
admin := acp.Caller{UserID: uuid.New(), IsAdmin: true}
user := acp.Caller{UserID: uuid.New(), IsAdmin: false}
_, _ = svc.Create(ctx, admin, acp.CreateAgentKindInput{
Name: "k1", DisplayName: "k1", BinaryPath: "x", Enabled: true,
})
_, _ = svc.Create(ctx, admin, acp.CreateAgentKindInput{
Name: "k2", DisplayName: "k2", BinaryPath: "x", Enabled: false,
})
adminList, _ := svc.List(ctx, admin)
userList, _ := svc.List(ctx, user)
assert.Len(t, adminList, 2)
assert.Len(t, userList, 1)
}
func TestAgentKindService_Update_PatchSemantics(t *testing.T) {
t.Parallel()
ctx := context.Background()
repo := newFakeAgentKindRepo()
rec := &agentKindRecordingRecorder{}
svc := acp.NewAgentKindService(repo, testEncryptor(t), rec)
admin := acp.Caller{UserID: uuid.New(), IsAdmin: true}
k, _ := svc.Create(ctx, admin, acp.CreateAgentKindInput{
Name: "k", DisplayName: "K", BinaryPath: "/x", Enabled: true,
})
disp := "Renamed"
updated, err := svc.Update(ctx, admin, k.ID, acp.UpdateAgentKindInput{DisplayName: &disp})
require.NoError(t, err)
assert.Equal(t, "Renamed", updated.DisplayName)
assert.Equal(t, "/x", updated.BinaryPath) // 未改
last := rec.entries[len(rec.entries)-1]
assert.Equal(t, "acp.agent_kind.update", last.Action)
}