You've already forked agentic-coding-workflow
ACP / MCP 完善
This commit is contained in:
@@ -12,6 +12,7 @@ package acp_test
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -25,12 +26,13 @@ import (
|
||||
"github.com/yan1h/agent-coding-workflow/internal/audit"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/notify"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/mcp"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/workspace"
|
||||
)
|
||||
|
||||
// newSupervisorForTest 构造一个绑定到 PG 真实 audit Recorder 的 Supervisor,
|
||||
// 使用静音 logger + 默认 SupervisorConfig(5s SpawnTimeout 给 fake agent 留 buffer)。
|
||||
// withAudit=false 时 Recorder 为 nil,对应 onExit 跳过 audit 路径。
|
||||
func newSupervisorForTest(t *testing.T, pool *pgxpool.Pool, repo acp.Repository, withAudit bool, mcpTokens mcp.TokenService) *acp.Supervisor {
|
||||
func newSupervisorForTest(t *testing.T, pool *pgxpool.Pool, repo acp.Repository, withAudit bool, mcpTokens mcp.TokenService, wtSvc workspace.WorktreeService) *acp.Supervisor {
|
||||
t.Helper()
|
||||
var rec audit.Recorder
|
||||
if withAudit {
|
||||
@@ -39,7 +41,7 @@ func newSupervisorForTest(t *testing.T, pool *pgxpool.Pool, repo acp.Repository,
|
||||
disp := notify.NewDispatcher(notifyRepoStub{}, slogDiscard())
|
||||
return acp.NewSupervisor(
|
||||
repo, rec, disp,
|
||||
nil, nil, // wtSvc / wsRepo: tests 用 IsMain=true,走 repo.ReleaseMainWorktree
|
||||
wtSvc, nil, // wtSvc: IsMain=false 测试注入 fake;wsRepo: 测试不走该路径
|
||||
mcpTokens,
|
||||
testEncryptor(t),
|
||||
acp.SupervisorConfig{
|
||||
@@ -88,7 +90,7 @@ func TestSupervisor_HappyPath_Spawn_Handshake(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
sup := newSupervisorForTest(t, pool, repo, true, nil)
|
||||
sup := newSupervisorForTest(t, pool, repo, true, nil, nil)
|
||||
proc, err := sup.Spawn(ctx, sess, k, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, proc)
|
||||
@@ -217,7 +219,7 @@ func TestSupervisor_AgentCrashes(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
sup := newSupervisorForTest(t, pool, repo, true, nil)
|
||||
sup := newSupervisorForTest(t, pool, repo, true, nil, nil)
|
||||
proc, err := sup.Spawn(ctx, sess, k, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, proc)
|
||||
@@ -306,7 +308,7 @@ func TestSupervisor_OnExit_RevokesMCPToken(t *testing.T) {
|
||||
require.True(t, ok)
|
||||
|
||||
fakeTokens := &fakeSupMCPTokens{}
|
||||
sup := newSupervisorForTest(t, pool, repo, true, fakeTokens)
|
||||
sup := newSupervisorForTest(t, pool, repo, true, fakeTokens, nil)
|
||||
proc, err := sup.Spawn(ctx, sess, k, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, proc)
|
||||
@@ -332,3 +334,86 @@ func TestSupervisor_OnExit_RevokesMCPToken(t *testing.T) {
|
||||
require.Len(t, fakeTokens.revoked, 1, "expected exactly one RevokeBySession call")
|
||||
assert.Equal(t, sess.ID, fakeTokens.revoked[0], "RevokeBySession should receive the session ID")
|
||||
}
|
||||
|
||||
// fakeWorktreeSvc 实现 workspace.WorktreeService,仅记录 ReleaseByHolder 收到的
|
||||
// holder 字符串,用于验证 onExit 的子 worktree 释放路径。
|
||||
type fakeWorktreeSvc struct {
|
||||
mu sync.Mutex
|
||||
holders []string
|
||||
}
|
||||
|
||||
func (f *fakeWorktreeSvc) Create(_ context.Context, _ workspace.Caller, _ uuid.UUID, _ workspace.CreateWorktreeInput) (*workspace.Worktree, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeWorktreeSvc) List(_ context.Context, _ workspace.Caller, _ uuid.UUID) ([]*workspace.Worktree, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeWorktreeSvc) Delete(_ context.Context, _ workspace.Caller, _ uuid.UUID) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakeWorktreeSvc) Acquire(_ context.Context, _ workspace.Caller, _ uuid.UUID) (*workspace.Worktree, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeWorktreeSvc) Release(_ context.Context, _ workspace.Caller, _ uuid.UUID) (*workspace.Worktree, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeWorktreeSvc) ReleaseByHolder(_ context.Context, holder string) ([]*workspace.Worktree, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.holders = append(f.holders, holder)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// holdersSnapshot 拷贝当前已记录的 holder 列表(onExit 在 monitor goroutine 中调用)。
|
||||
func (f *fakeWorktreeSvc) holdersSnapshot() []string {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return append([]string(nil), f.holders...)
|
||||
}
|
||||
|
||||
// TestSupervisor_OnExit_ReleasesSubWorktree 验证:IsMainWorktree=false 的 session
|
||||
// 退出后,onExit 通过 wtSvc.ReleaseByHolder("session:<sid>") 释放子 worktree。
|
||||
func TestSupervisor_OnExit_ReleasesSubWorktree(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
repo, pool := setupRepo(t)
|
||||
|
||||
uid := mustInsertUser(t, ctx, pool, "sup-wt@local", false)
|
||||
pid, wsid := mustInsertProjectWorkspace(t, ctx, pool, uid)
|
||||
|
||||
bin := fakeAgentBinary(t)
|
||||
k, err := repo.CreateAgentKind(ctx, &acp.AgentKind{
|
||||
ID: uuid.New(), Name: "fake-wt", DisplayName: "Fake",
|
||||
BinaryPath: bin, Args: []string{},
|
||||
Enabled: true, CreatedBy: uid,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
sess, err := repo.InsertSession(ctx, &acp.Session{
|
||||
ID: uuid.New(), WorkspaceID: wsid, ProjectID: pid,
|
||||
AgentKindID: k.ID, UserID: uid,
|
||||
Branch: "feat-x", CwdPath: t.TempDir(), IsMainWorktree: false, Status: acp.SessionStarting,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
fakeWt := &fakeWorktreeSvc{}
|
||||
sup := newSupervisorForTest(t, pool, repo, true, nil, fakeWt)
|
||||
proc, err := sup.Spawn(ctx, sess, k, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, proc)
|
||||
|
||||
sup.Kill(ctx, sess.ID, 1*time.Second)
|
||||
|
||||
// onExit 在 monitor goroutine 中异步执行;轮询 fake 直到记录到 release 调用。
|
||||
deadline := time.After(5 * time.Second)
|
||||
for len(fakeWt.holdersSnapshot()) == 0 {
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatal("ReleaseByHolder never called after process exit")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
holders := fakeWt.holdersSnapshot()
|
||||
require.Len(t, holders, 1, "expected exactly one ReleaseByHolder call")
|
||||
assert.Equal(t, "session:"+sess.ID.String(), holders[0])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user