Files
Agent-Squad/squad/request_v2_test.go
weichenandCursor a182d7662b Move cross-library squad code into adapter modules
Why: the squad core has to compile without agent-native or agent-intent. Ports stay in the core; adapters and the CLI own the concrete clients.
Evidence: GOWORK=off go test -count=1 ./squad/ ./store/ ./textextract/ ./schema/ passed, and the intent, sqlite, native, and cmd modules passed their own go test.
Impact: core go.mod keeps error-code-system only. Nested gitlinks stay. Callers register the native backend through agent-squad/adapters/native.
Refs: D1
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 13:40:58 +00:00

154 lines
5.0 KiB
Go

package squad
import (
"context"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
)
// captureBackend 记录收到的请求(v2 字段贯通断言用)。
type captureBackend struct {
mu sync.Mutex
reqs []ExecRequest
script map[string]string // 子目录名 → 单一目标内容
}
func (b *captureBackend) Name() string { return "capture" }
func (b *captureBackend) Execute(ctx context.Context, req ExecRequest) (*ExecResult, error) {
b.mu.Lock()
b.reqs = append(b.reqs, req)
b.mu.Unlock()
key := filepath.Base(filepath.Clean(req.WorkDir))
content := b.script[key]
if content == "" {
content = "# 默认交付物\n\n脚本后端生成的合格内容,长度满足最小质检要求。\n"
}
patches := map[string][]byte{}
for _, t := range req.Targets {
out := filepath.Join(req.WorkDir, t)
_ = os.MkdirAll(filepath.Dir(out), 0o755)
_ = os.WriteFile(out, []byte(content), 0o644)
patches[t] = []byte(content)
}
return &ExecResult{Success: true, Patches: patches}, nil
}
func TestStepRequestCarriesRoleSpecV2Fields(t *testing.T) {
dir := t.TempDir()
reg := NewRoleRegistry()
role := RoleSpec{
Role: "researcher", Name: "调研", Backend: "capture",
Tools: []string{"read_file", "grep"}, Skills: []string{"docx-author"},
}
if err := reg.Register(role); err != nil {
t.Fatal(err)
}
capture := &captureBackend{}
backends := NewBackendResolver()
backends.RegisterInstance("capture", capture)
engine, err := NewEngine(Options{
DataDir: dir, Registry: reg,
Planner: mockScriptPlanner(StepSpec{ID: "s1", Role: "researcher", Title: "调研", Targets: []string{"output.md"}}),
Backends: backends, Sink: NopSink{},
})
if err != nil {
t.Fatal(err)
}
res, err := engine.Run(context.Background(), Goal{Text: "生成测试报告"}, RunOptions{})
if err != nil {
t.Fatal(err)
}
if res.Status != RunCompleted {
t.Fatalf("want completed, got %s (err=%s)", res.Status, res.Error)
}
if len(capture.reqs) == 0 {
t.Fatal("capture backend never invoked")
}
req := capture.reqs[0]
if len(req.ToolAllowlist) != 2 || req.ToolAllowlist[0] != "read_file" {
t.Fatalf("role.Tools must flow into ToolAllowlist, got %v", req.ToolAllowlist)
}
if len(req.Skills) != 1 || req.Skills[0] != "docx-author" {
t.Fatalf("role.Skills must flow into request, got %v", req.Skills)
}
if !strings.Contains(req.SessionScope, "s1") || !strings.Contains(req.ExecutionID, "s1") {
t.Fatalf("identifiers missing: scope=%q exec=%q", req.SessionScope, req.ExecutionID)
}
if req.Budget == nil || time.Until(req.Budget.Deadline) <= 0 {
t.Fatal("step budget deadline must be set and in the future")
}
}
func TestDeriveInheritsExecutionConfig(t *testing.T) {
reg := NewRoleRegistry()
base := RoleSpec{
Role: "worker", Backend: "agent-native", Model: "m", ToolProfile: "workspace",
Tools: []string{"read_file"}, Skills: []string{"s1"}, BashPermission: "ask",
}
if err := reg.Register(base); err != nil {
t.Fatal(err)
}
derived, err := reg.Derive(DeriveRequest{
RoleKey: "py-worker", BaseRole: "worker",
Tools: []string{"read_file", "run_python"},
Skills: []string{"s1", "xlsx-author"},
ToolProfile: "workspace",
BashPermission: "allow",
})
if err != nil {
t.Fatal(err)
}
if derived.ToolProfile != "workspace" || derived.BashPermission != "allow" {
t.Fatalf("derive must honor explicit exec config: %+v", derived)
}
if len(derived.Tools) != 2 || len(derived.Skills) != 2 {
t.Fatalf("derive must take request slices over base: tools=%v skills=%v", derived.Tools, derived.Skills)
}
// 未显式给定时继承基线。
inherits, err := reg.Derive(DeriveRequest{RoleKey: "plain", BaseRole: "worker"})
if err != nil {
t.Fatal(err)
}
if inherits.BashPermission != "ask" || len(inherits.Tools) != 1 {
t.Fatalf("derive must inherit base exec config: %+v", inherits)
}
}
func TestRoleHitlPolicyOverridesGlobal(t *testing.T) {
dir := t.TempDir()
reg := NewRoleRegistry()
// bash=ask + 角色级 auto:不再挂起等审批,而是直接失败(auto 无值守语义)。
if err := reg.Register(RoleSpec{
Role: "runner", Name: "执行", Backend: "capture", BashPermission: BashPermissionAsk,
HitlPolicy: &HitlPolicy{Mode: HitlAuto},
}); err != nil {
t.Fatal(err)
}
capture := &captureBackend{}
backends := NewBackendResolver()
backends.RegisterInstance("capture", capture)
engine, err := NewEngine(Options{
DataDir: dir, Registry: reg,
Planner: mockScriptPlanner(StepSpec{ID: "s1", Role: "runner", Title: "跑", Targets: []string{"output.md"}}),
Backends: backends, Sink: NopSink{},
})
if err != nil {
t.Fatal(err)
}
res, err := engine.Run(context.Background(), Goal{Text: "生成测试报告"}, RunOptions{})
if err != nil {
t.Fatal(err)
}
if res.Status != RunFailed || !strings.Contains(res.Error, "permission denied") {
t.Fatalf("role auto policy must fail the bash=ask step instead of holding, got %s err=%s", res.Status, res.Error)
}
if len(capture.reqs) != 0 {
t.Fatal("unapproved bash step must never execute")
}
}